268
2011 Jasmin Blanchete & Mark Summerfield Zona Qt 01/01/2011 C++ - Programación GUI con Qt 4

C++ Programacion GUI Con Qt 4 1st Ed

Embed Size (px)

Citation preview

2011

Jasmin Blanchete & Mark

Summerfield

Zona Qt

01/01/2011

C++ - Programación GUI con Qt 4

2 6. Manejo de Layouts

3 6. Manejo de Layouts

C++ - Programación GUI con Qt 4

Jasmin Blanchete

Mark Summerfield

Esta es una traducción libre y no oficial realizada por Zona Qt únicamente con fines educativos

Esta es una traducción libre y no oficial realizada por Zona Qt únicamente con fines educativos

4 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 Múltiples Documentos .............................................................................. 22

7. Procesamiento de Eventos .............................................................................................. 30

Reimplementar Manejadores de Eventos ................................................................... 30

Instalar Filtros de Eventos ........................................................................................... 35

Evitar Bloqueos Durante Procesamientos Intensivos .................................................. 37

8. Gráficos En 2 y 3 Dimensiones......................................................................................... 40

Dibujando con QPainter............................................................................................... 40

Transformaciones ........................................................................................................ 45

Renderizado de Alta Calidad con QImage ................................................................... 51

Impresión ..................................................................................................................... 53

Gráficos 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 Genéricos ................................................................................................ 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

5 6. Manejo de Layouts

Incrustando Recursos ................................................................................................ 133

Comunicación 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 “What´s This?” ........................................................ 188

Usando QTextBrowser como un Mecanismo de Ayuda ............................................ 190

Usando el Qt Assistant como una Poderosa Ayuda En Línea .................................... 193

Parte III Qt Avanzado ................................................................................................................ 195

17. Internacionalización .................................................................................................... 196

Trabajando con Unicode ............................................................................................ 197

Haciendo Aplicaciones que Acepten Traducciones ................................................... 201

Cambio Dinámico del Lenguaje ................................................................................. 206

Traduciendo Aplicaciones .......................................................................................... 210

18. Multithreading ............................................................................................................. 214

Creando Threads ........................................................................................................ 214

Sincronizando Threads ............................................................................................... 217

Comunicándose 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. Características Específicas de Plataformas .................................................................. 243

Creando Interfaces con APIs Nativas ........................................................................ 243

Usando Activex en Windows ..................................................................................... 247

Manejando la Administración de Sesión en X11 ....................................................... 257

6 6. Manejo de Layouts

21. Programación Embebida ............................................................................................. 264

Iniciando con Qtopia .................................................................................................. 264

Personalizando Qtopia Core ...................................................................................... 266

Glosario .............................................................................................................................. 268

7 6. Manejo de Layouts

Parte II Qt Intermedio

8 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 Múltiples Documentos

A cada widget que colocamos en un formulario se le debe dar un tamaño y una posición adecuada. Al

proceso de organizar el tamaño y la posición de los widgets sobre un formulario se lo denomina en inglés

"layout". Qt provee varias clases que nos ayudarán con esta tarea: QHBoxLayout, QVBoxLayout,

QGridLayout y QStackLayout. Estas clases son cómodas y fáciles de usar, al punto tal, que casi todos

los desarrolladores las emplean, ya sea directamente en el código fuente o a través del diseñador visual Qt

Designer.

Otra razón para usar estas clases, es que nos aseguran que los formularios se adaptarán automáticamente a

las diferentes fuentes, idiomas y plataformas usadas. Si el usuario cambia alguna configuración de fuentes

del sistema, los formularios de la aplicación responderán inmediatamente, cambiando su tamaño si es

necesario. Y si se traduce la interfaz del programa a otros lenguajes, estas clases considerarán 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 común estas clases es que proveen un mecanismo de

disposición flexible que el usuario puede manipular a su antojo. Por ejemplo QSplitter ofrece una barra

divisora que se puede arrastrar para cambiar el tamaño del widget y QWorkspace provee soporte para

MDI (Interfaz de Múltiples Documentos), una manera de mostrar simultáneamente varios documentos dentro

de la ventana principal de la aplicación. Estas clases se incluyen en este capítulo porque se usan muy a

menudo como alternativas a las propias clases de layout.

Organizando Widgets en un Formulario

Hay tres formas básicas 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 diálogo Buscar Archivo mostrado en la Figura 6.1 como ejemplo.

9 6. Manejo de Layouts

Figura 6.1. El diálogo Buscar Archivo

El posicionamiento absoluto es la técnica más engorrosa para acomodar nuestros widgets. Esta técnica se

basa en asignar tamaños y posiciones fijas a los widgets y al formulario. El constructor de

DialogoBuscarArchivo luciría 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 tamaño de la ventana.

Algunos textos pueden ser truncados si el usuario elige una fuente demasiado grande o si la

aplicación es traducida a otro lenguaje.

El widget podría tener un tamaño inapropiado en algunos estilos.

Las posiciones y los tamaños deben ser calculados manualmente. Esto es tedioso, propenso a errores,

y hace que el mantenimiento sea difícil.

Una alternativa al posicionamiento absoluto es el layout manual. Con esta técnica los widgets aun se colocan

en posiciones absolutas, pero sus tamaños se mantienen proporcionales al tamaño de la ventana en vez de ser

invariantes. Para establecer los tamaños de los widgets del formulario, se re implementa la función

resizeEvent() del mismo:

DialogoBuscarArchivo::DialogoBuscarArchivo(QWidget *parent)

:QDialog(parent)

{

10 6. Manejo de Layouts

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 tamaño mínimo del formulario a 265 x 190

y el tamaño 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 tamaño.

Al igual que en el posicionamiento absoluto, en el layout manual se requiere hacer algunos cálculos por parte

del programador. Escribir este tipo de código es agotador, especialmente si el diseño del formulario cambia.

Y todavía existe el riesgo de que se trunquen los textos. Podemos evitar este riesgo tomando en cuenta los

tamaños recomendados para los widgets del formulario, pero eso complicaría el código aun más.

Figura 6.2. Redimensionando un dialogo escalable

La solución más conveniente para organizar los widgets en un formulario es usar los administradores de

layout provistos por Qt. Estos nos proporcionan parámetros por defecto para cada tipo de widget y toman en

cuenta el tamaño recomendado para cada widget, que a su vez, depende de la fuente, el estilo o el contenido

del widget. También respetan los tamaños mínimos y máximos establecidos, y automáticamente ajustan el

diseño en respuesta a cambios de: fuentes, contenido o tamaño de la ventana.

11 6. Manejo de Layouts

Las tres clases más importantes son: QHBoxLayout, QVBoxLayout y QGridLayout. Estas heredan de

QLayout, la cual provee el marco básico para las operaciones de layout. Las tres clases son totalmente

soportadas por Qt Designer y también pueden ser usadas directamente en el código.

Este es el código 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 disposición 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 diálogo y el espacio entre los

widgets están 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().

Podríamos crear visualmente el mismo diálogo en Qt Designer de la siguiente manera: colocamos los

widgets en su posición 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 Capítulo 2 para crear los

diálogos Ir-a-Celda y Ordenar de la aplicación Hoja de Cálculo.

Utilizar QHBoxLayout y QVBoxLayout es bastante sencillo, pero usar QGridLayout es un poco más

complicado. QGridLayout trabaja como una cuadricula o rejilla bidimensional de celdas. El QLabel

ubicado en la esquina superior izquierda ocupa la posición (0,0), y su correspondiente QLineEdit ocupa la

posición (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 también se extienden por dos columnas. El llamado a la

función 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.

12 6. Manejo de Layouts

Figura 6.3. Organización de widgets del diálogo Buscar Archivo

La función addStretch() agrega un elemento de estiramiento en el lugar que le indiquemos, para

proveer espacio en blanco. Al añadir un elemento de estiramiento, le estamos indicando al administrador de

layout que coloque todo el espacio sobrante entre el botón Cerrar y el botón 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á automáticamente a la nueva situación. Lo mismo

ocurre cuando se llama a los métodos hide() o show() de algún widget. Si el tamaño recomendado de un

widget cambia, entonces el layout se actualiza automáticamente para ajustarse a la nueva situación. Los

administradores de layout pueden establecer automáticamente el tamaño mínimo para el formulario de

manera general, basándose en los tamaños mínimos 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 disposición de los widgets cambiando las políticas de tamaño y el tamaño recomendado de los

widgets.

La política de tamaño de un widget le dice al sistema de layout cómo este debería estirarse o encogerse. Qt

provee políticas de tamaños predeterminadas para todos sus widgets, pero como un solo valor no puede

servir para todas las situaciones posibles, es común que los desarrolladores cambien las políticas de tamaño

de algún o algunos widgets del formulario. QSizePolicy tiene un componente vertical y uno horizontal.

Estos serían los valores más útiles:

• Fixed: el widget no puede agrandarse ni achicarse. Siempre permanecerá con el tamaño recomendado.

• Minimum: el tamaño recomendado del widgets es su tamaño mínimo. Su tamaño no podrá ser más

pequeño de lo que indique la propiedad de tamaño recomendado (sizeHint), pero sí podrá crecer para

ocupar el espacio disponible si es necesario.

• Maximum: el tamaño recomendado del widget es su tamaño máximo. Solo podrá achicarse hasta su tamaño

mínimo.

• Preferred: el tamaño recomendado es el tamaño deseado. Puede crecer o achicarse si es necesario.

• Expanding: el widget puede estirarse o encogerse, pero está más dispuesto a expandirse.

La Figura 6.4 muestra el comportamiento de las diferentes políticas de tamaño usando un QLabel con el

texto “Algún Texto” como ejemplo.

13 6. Manejo de Layouts

Figura 6.4. El propósito de las diferentes políticas de tamaño

Como se ve en la figura, tanto Preferred como Expanding parecen tener el mismo comportamiento. Se

preguntarán: ¿Qué es entonces lo que tienen de diferente? Cuando se cambia el tamaño 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 mantendrán su tamaño.

Hay otras dos políticas de tamaño: 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 método es

combinar Expanding con una re implementación de minimumSizeHint(). El segundo es similar a

Expanding, excepto que ignora tanto el tamaño recomendado del widget, como su tamaño mínimo.

Adicionalmente a los componentes verticales y horizontales de las políticas de tamaño, La clase

QSizePolicy almacena dos factores de expansión: uno horizontal y otro vertical. Estos pueden ser usados

para indicar cómo se deberían 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 tamaño mínimo, el tamaño máximo o un tamaño 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 páginas y muestra solo una a la vez,

ocultando las otras de la vista del usuario. Esta clase es invisible y su edición no proporciona ningún

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 diseño de la interfaz. Qt también incluye QStackedWidget para proveer un widget

con un paginado pre construido.

La numeración de las páginas comienza en 0. Para que un determinado widget sea visible, se debe llamar a

setCurrentIndex() con el número de página a mostrar como argumento. Para obtener el número de

página de un widget se usa indexOf().

14 6. Manejo de Layouts

Figura 6.5. QStackedLayout

Figura 6.6. Dos páginas del dialogo Preferencias

El diálogo 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 página diferente del QStackedLayout. Este es el código más

relevante del constructor del diálogo:

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)), stackedLayout,

SLOT(setCurrentIndex(int)));

•••

widgetLista->setCurrentRow(0);

}

15 6. Manejo de Layouts

Se crea un QListWidget y se rellena con los nombres de las páginas. Luego se crea el

QStackedLayout y se agrega cada página con la función addWidget(). Conectamos la señal

currentRowChanged(int) de la lista al slot setCurrentIndex(int) del layout para implementar

el cambio de páginas, finalmente se llama a la función setCurrentRow() de la lista para seleccionar la

página número 0.

Este tipo de formularios son muy fáciles 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 página con los widgets necesarios y ajustar el layout. (Para crear una nueva página,

solo basta con hacer clic con el botón derecho y elegir Insert Page; para cambiar de página 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 señal 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 páginas usando señales y slots predefinidos, el diálogo 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 están separados por un

divisor. Este nos permite modificar el tamaño 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 más control al

usuario.

Los widgets de un QSplitter se van colocando automáticamente 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 código 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();

}

16 6. Manejo de Layouts

Figura 6.7. La aplicación Separador

El ejemplo consta de tres QTextEdit dispuestos horizontalmente en un QSplitter. A diferencia de los

administradores de layouts, los cuales no tienen una representación visual, QSplitter hereda de

QWidget, y por lo tanto puede ser usado como cualquier otro widget.

Figura 6.8. Widgets de la aplicación Separador

Combinando QSplitters horizontales y verticales podemos lograr interfaces bastante complejas. Por

ejemplo, la aplicación 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 aplicación Cliente de Correo en Mac OS X

17 6. Manejo de Layouts

Este es el código del constructor de la ventana principal de la aplicación 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();

}

Después 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 tamaño de la ventana, un QSplitter normalmente distribuye el espacio de

manera tal que el tamaño 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 tamaño,

mientras QTextEdit consumirá todo el espacio sobrante. Para lograr esto tenemos que usar la función

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 indexación de separadores de la aplicación 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 posición 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 posición 1

(splitterDerecho) tenga un factor de crecimiento de 1. Esto nos asegura que el QTextEdit tomará el

espacio sobrante disponible.

Cuando se inicia la aplicación, QSplitter le asigna a cada widget un tamaño apropiado basándose en el

tamaño inicial de cada uno (o en su tamaño recomendado si no se especifica un tamaño inicial). Podemos

18 6. Manejo de Layouts

mover los divisores mediante la función QSplitter::setSizes(). La clase QSplitter nos ofrece la

posibilidad de guardar su estado y restablecerlo la próxima vez que ejecutemos la aplicación. El siguiente

código muestra cómo la función guardarConfiguraciones() se encarga de guardar el estado de los

widgets de la aplicación Cliente de Correo:

void ClienteCorreo::guardarConfiguraciones()

{

QSettings config("Software Inc.", "Cliente de Correo");

config.beginGroup("ventanaPrincipal");

config.setValue("tamaño", size());

config.setValue("splitterPrincipal", splitterPrincipal->saveState());

config.setValue("splitterDerecho", splitterDerecho->saveState());

config.endGroup();

}

Con la función leerConfiguraciones() recuperamos los estados previamente guardados:

void ClienteCorreo::leerConfiguraciones()

{

QSettings config("Software Inc.", "Cliente de Correo");

config.beginGroup("ventanaPrincipal");

resize(config.value("tamaño", 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 posición que deseamos, seleccionarlos y luego hacer clic en la opción 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 más 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 función setWidget() pasándole como argumento el

widget al que queremos dotar de barras de desplazamiento. QScrollArea automáticamente cambia de

padre al widget para hacerlo hijo del viewport o puerto de vista (accesible a través de

19 6. Manejo de Layouts

QScrollArea::vewPort()), si es que ya no lo es. Por ejemplo, si queremos agregar barras de

desplazamiento al widget IconEditor (desarrollado en el Capítulo 5), escribimos el siguiente código:

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 tamaño actual o usará el tamaño recomendado si no se suministra

información de tamaño. Por medio de la función setWidgetResizable(true) podemos hacer que

QScrollArea haga uso de cualquier espacio extra más allá de su tamaño recomendado.

Por defecto, las barras de desplazamiento solo aparecen cuando la vista es más pequeña que el widget.

Podemos forzar a que se muestren siempre las barras de desplazamiento estableciendo las políticas 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.

20 6. Manejo de Layouts

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 título, aun cuando esté anclado. Se pueden mover de un área

a otra solo arrastrándolo desde su barra de título. También 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

botón de cerrar que se encuentra en la barra de título de la misma. Cualquier combinación de estas

características puede ser desactivada por medio de QDockWidget::setFeatures().

En versiones anteriores de Qt, las barras de herramientas eran tratadas como widgets acoplables y

compartían las mismas áreas. Desde la versión 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 líneas punteadas pueden pertenecer a cualquiera de las dos áreas contiguas. Por

ejemplo, podríamos hacer que la esquina superior izquierda perteneciera al área de anclaje izquierda

llamando al métodoQMainWindow::setCorner(Qt::TopLeftCorner, Qt::LeftDockWid-

getArea).

Figura 6.13. Un QMainWindow con un dock widget

21 6. Manejo de Layouts

Figura 6.14. Dock area y toolbar area de QMainWindow

El siguiente fragmento de código muestra cómo 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 función setAllowedAreas() especifica las restricciones sobre cuáles á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 ningún área, se pueden usar cualquiera de las cuatro para acoplar el widget.

A continuación, se muestra cómo 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 posición de los widgets acoplables y las barras de herramientas para poder

restablecer su ubicación la próxima vez que ejecutemos la aplicación, podemos usar un código 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("tamaño", size());

config.setValue("estado", saveState());

22 6. Manejo de Layouts

config.endGroup();

}

void MainWindow::leerConfiguraciones()

{

QSettings config("Software Inc.", "Editor de Iconos");

config.beginGroup("ventanaPrincipal");

resize(config.value("tamaño").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 Múltiples Documentos

A las aplicaciones que pueden albergar varios documentos dentro de su área central se las denomina

aplicaciones de interfaz de múltiples documentos (o MDI para abreviar). En Qt, una aplicación MDI se crea

usando la clase QWorkspace como widget central y haciendo que cada documento sea hija de ésta.

Es una convención 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 sección desarrollaremos la aplicación (tipo editor de texto) MDI Editor (Figura 6.16), para

ejemplificar cómo se crea una aplicación MDI y cómo implementar un menú Ventana.

La aplicación consta de dos clases: MainWindow y Editor. Debido a que el código es muy similar al

código de la aplicación desarrollada en la Parte I, nos centraremos en el código 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"));

23 6. Manejo de Layouts

}

Figura 6.16. La aplicación MDI Editor

Figura 6.17. Menús de la aplicación MDI Editor

En el constructor, creamos el widget QWorkspace y lo transformamos en el widget central. Conectamos la

señal 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 opción de menú Archivo|Nuevo. Este depende de la función

privada crearEditor() para crear un widget Editor.

Editor *MainWindow::crearEditor()

{

Editor *editor = new Editor;

connect(editor, SIGNAL(copyAvailable(bool)), accionCortar,

24 6. Manejo de Layouts

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 función crearEditor() se encarga de crear un widget Editor y establecer la conexión de dos

señales, que nos asegurarán que las acciones Edición|Cortar y Edición|Copiar se activarán o

desactivarán 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

pequeño inconveniente, ya que nos interesa que la señal copyAvailable(bool) solo provenga de la

ventana activa, no de otras. Pero esta señal solo puede ser emitida por la ventana activa, así que en la

práctica, no sería un problema.

Una vez creado y configurado el Editor, agregamos un QAction que representa a la ventana en el menú

Ventana. La acción es provista por la clase Editor, la cual cubriremos en un momento. También

agregamos la acción 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 función abrir() se corresponde a la opción del menú Archivo|Abrir. Aquí se crea un Editor para

el nuevo documento y se llama a la función abrir() del mismo. Tiene más 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 demás.

Si esta función falla, simplemente cerramos el editor ya que el usuario ya ha sido notificado del error. No

necesitamos eliminar explícitamente el objeto Editor, esto se realiza automáticamente 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 función Editor::guardar() del editor activo, si es que hay uno.

Nuevamente, el código que realiza el verdadero trabajo, está en la clase Editor.

Editor *MainWindow::editorActivo()

{

return qobject_cast<Editor *>(workspace->activeWindow());

}

La función privada editorActivo() nos devuelve un puntero al objeto Editor de la ventana activa, o

un puntero nulo si no hay ventana activa.

25 6. Manejo de Layouts

void MainWindow::cortar()

{

if (editorActivo())

editorActivo()->cortar();

}

El slot cortar() llama a la función Editor::cortar() del editor activo. No se mostrarán los slots

copiar() y pegar() porque tienen el mismo patrón.

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 mayoría 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 acción 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 función privada crearMenus() se encarga de agregar las acciones al menú Ventana. Estas son las

acciones típicas de este menú y son implementadas a través los slots closeActiveWindow(),

closeAllWindows(), tile(), y cascade() de la clase QWorkspace.

26 6. Manejo de Layouts

Cada vez que se abre una nueva ventana, esta es agregada a la lista del menú Ventana (esto se realiza en la

función crearEditor()). Cuando se cierra una ventana, la opción correspondiente es borrada (ya que el

editor es el padre de la acción) y removida del menú Ventana.

void MainWindow::closeEvent(QCloseEvent *event)

{

workspace->closeAllWindows();

if (editorActivo()) {

event->ignore();

} else {

event->accept();

}

}

La función 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 aplicación entera. Si no implementáramos

closeEvent() en MainWindow, el usuario no dispondría de la oportunidad de guardar cambios

pendientes en los documentos.

Hemos terminado con la revisión de la clase MainWindow, ahora pasaremos a la implementación de la

clase Editor. Esta clase representa una ventana de edición de documentos. Hereda de QTextEdit, la cual

nos provee la funcionalidad de edición de texto. Como cualquier otro widget puede ser usado como una

ventana autónoma y por lo tanto también como una ventana hija de un MDI.

Esta es la definición 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;

};

27 6. Manejo de Layouts

Las funciones privadas okParaContinuar(), guardaArchivo(), setArchivoActual() y

soloNombre(), están presentes en la clase MainWindow de aplicación Hoja de Cálculo desarrollada en

la Parte I, por lo que no se explicarán.

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 aplicación 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 método más común

consiste en asignar un número 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 señal 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.

Después 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 función nuevoArchivo() genera un nombre para el nuevo documento (de tipo documento1.txt).

El código no se colocó en el constructor para no consumir números si se desea abrir un archivo existente en

vez de crear uno nuevo. Como definimos estática a la variable numeroDocumento puede ser compartida

por todos los objetos Editor creados.

El marcador "[*]" en el título 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()

{

28 6. Manejo de Layouts

QString nombreArchivo = QFileDialog::getOpenFileName(this,

tr("Abrir"), ".", filtros);

if (nombreArchivo.isEmpty())

return false;

return abrirArchivo(nombreArchivo);

}

La función abrir() intenta abrir un archivo existente por medio de la función abrirArchivo().

bool Editor::guardar()

{

if (isSinTitulo) {

return guardarComo();

} else {

return guardaArchivo(archivoActual);

}

}

La función guardar() determina por medio de la variable isSinTitulo si debería llamar a la función

guardaArchivo() o a la función guardarComo().

void Editor::closeEvent(QCloseEvent *event)

{

if (okParaContinuar()) {

event->accept();

} else {

event->ignore();

}

}

La función closeEvent() es re implementada para permitirle al usuario guardar cambios pendientes. La

lógica está codificada en la función 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 función setArchivoActual() es llamada desde abrirArchivo() y guardaArchivo() para

actualizar las variables isSinTitulo y archivoActual, establecer el título de la ventana y el texto de

la acción, y colocar en false la propiedad "modified" del documento. Cuando sea que el usuario modifique

el texto, el QTextDocument subyacente emitirá la señal contentsChanged() y establecerá

"modified" a true.

QSize Editor::sizeHint() const

{

return QSize(72 * fontMetrics().width(‟x‟), 25 *

fontMetrics().lineSpacing());

}

La función sizeHint() devuelve un objeto QSize basado en el ancho de la letra "x" y el alto de una

línea de texto. Este es usado por QWorkspace para darle un tamaño inicial a la ventana.

29 6. Manejo de Layouts

Este es el código 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 línea de comandos y se especifica nombres de archivo, se intentarán

cargar. Si no se especifica nada, simplemente se iniciará la aplicación con un documento en blanco.

Las opciones de línea de comandos específicas de Qt (como -style y - font) son automáticamente

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 línea de comandos, QApplication::arguments() nos devolverá un QStringList

conteniendo dos ítems (“mdieditor” y “leeme.txt”) por lo que la aplicación se iniciará y abrirá el documento

leeme.txt.

MDI es una manera de manejar múltiples documentos al mismo tiempo. En MacOS X el enfoque preferido

es utilizar ventanas de nivel superior. Este tema fue expuesto en la sección "Documentos múltiples" del

Capítulo 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 botón del ratón, se genera un evento

para dicha acción; 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 mayoría de los eventos son

generados en respuesta a alguna acción 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 señales 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 señales. Se establece como regla que las señales son útiles cuando

usamos un widget, mientras que los eventos son útiles cuando implementamos un widget. Por ejemplo, si

trabajamos con un QPushButton, estaremos más interesados en su señal clicked() que en los procesos

de bajo nivel que causan su emisión. Pero si estamos desarrollando una clase que se comporta como un

QPushButton, necesitaremos escribir el código que controle los eventos del teclado y del ratón para poder

emitir la señal clicked() cuando sea necesario.

Reimplementar Manejadores de Eventos

En Qt, un evento es un objeto derivado de QEvent. Existen más de un centenar de tipos de eventos en Qt,

cada uno identificado por un valor de una enumeración. Podemos usar QEvent::type() para obtener el

valor de la enumeración que corresponde al evento emitido. Por ejemplo, QEvent::type() devolverá

QEvent::MouseButtonPress cuando se presiona un botón del ratón.

Muchos tipos de eventos requieren más información que puede ser guardada en un objeto QEvent; por

ejemplo, los eventos de ratón necesitan guardar cuál botón del ratón fue el que lo disparó, así como también

necesitan guardar la posición que tenía el puntero del mouse cuando sucedió el evento. Esta información

adicional es guardada en subclases de QEvent dedicadas, tal como QMouseEvent.

Los eventos son notificados a los objetos a través de la función event(), que se hereda de QObject. La

implementación de esta función en la clase QWidget re direcciona los tipos más comunes de eventos a

funciones que actúan como manejadores de eventos específicos, tales como mousePressEvent(),

keyPressEvent() y paintEvent().

31 7. Procesamiento de Eventos

Ya hemos visto varios manejadores de eventos cuando implementamos algunos ejemplos en capítulos

anteriores (MainWindow, EditorIcono y Plotter). Hay más tipos de eventos enumerados en la

documentación de QEvent y también es posible crear nuestros propios tipos de eventos y emitirlos por

nuestra cuenta. A continuación, revisaremos los dos tipos de eventos que merecen una mayor explicación: la

presión de teclas y los temporizadores.

Los eventos generados por la presión de teclas pueden ser controlados mediante la re implementación de las

funciones keyPressEvent() y keyReleaseEvent(). En la sección dedicada al widget Plotter, se

re implementó este último método. Normalmente solo necesitaremos enfocarnos en keyPressEvent()

ya que las únicas teclas que nos interesaría saber cuando se soltaron son: Ctrl , Shift y Alt (también 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 presión de

la teclas Inicio y Ctr+Inicio, la re implementación de keyPressEvent() se vería 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 presión 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 tabulación, antes

de llamar a keyPressEvent(). Este es el comportamiento habitual que queremos, pero para el widget

EditorCodigo, podríamos preferir la tecla Tab idente una línea. Aquí se muestra la re implementación 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 presión de una tecla, convertimos el objeto QEvent a QKeyEvent y

verificamos qué tecla ha sido presionada. Si fue la tecla Tab, realizamos algún 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 método de alto nivel para implementar atajos de teclado es usar QAction. Por ejemplo, si las funciones

irPrincipioDeLinea() e irPrincipioDeDocumento() son slots públicos del widget

EditorCodigo, y éste es usado como widget central en una clase MainWindow, podríamos

implementar atajos de teclados con el siguiente código:

32 7. Procesamiento de Eventos

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 fácil 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, podríamos 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) están 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 común de evento es el emitido por los temporizadores. Mientras que la mayor parte de los eventos

ocurre como resultado de una acción 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 más

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);

33 7. Procesamiento de Eventos

void timerEvent(QTimerEvent *evento);

void showEvent(QShowEvent *evento);

void hideEvent(QHideEvent *evento);

private:

QString miTexto;

int desplaz;

int miTimerId;

};

#endif

Como apreciarán, re implementamos cuatro controladores de evento, tres de los cuales todavía no hemos

visto: timerEvent(), showEvent() y hideEvent().

Ahora revisemos la implementación:

#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 ningún temporizador activo.

void Ticker::setTexto(const QString &nuevoTexto)

{

miTexto = nuevoTexto;

update();

updateGeometry();

}

La función 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 tamaño.

QSize Ticker::sizeHint() const

{

return fontMetrics().size(0, texto());

}

La función sizeHint() devuelve el tamaño ideal del widget, que se calcula como el espacio necesario

para el texto. QWidget::fontMetrics() nos devuelve un objeto QFontMetrics, el cual nos brinda

información relativa a la fuente del widget. En este caso, obtenemos el espacio necesario para dibujar el

texto, por medio de la función 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;

34 7. Procesamiento de Eventos

int x = -desplaz;

while (x < width()) {

painter.drawText(x, 0, anchoTexto, height(), Qt::AlignLeft |

Qt::AlignVCenter, texto());

x += anchoTexto;

}

}

La función 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 función showEvent() inicia un temporizador. Llama a QObject::startTimer(), la que nos

devuelve un número ID, que luego nos servirá para identificar al temporizador. QObject soporta varios

temporizadores independientes, cada cual con su propio intervalo de tiempo. Después de llamar a

startTimer(), Qt generará un evento cada 30 milisegundos aproximadamente; la precisión depende del

sistema operativo en donde se ejecute la aplicación.

Podríamos 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 función 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 más 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 función hideEvent() llama a QObject::killTimer() para detener el temporizador.

Los eventos de temporización 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 difícil de

mantener. En tales situaciones, es más fácil crear un QTimer por cada temporizador que necesitemos. Este

35 7. Procesamiento de Eventos

objeto emite la señal timeout() cuando se cumple el intervalo de tiempo establecido y también provee

una interfaz conveniente para temporizadores que solo se disparen una vez.

Instalar Filtros de Eventos

Una característica 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 solución sencilla,

para obtener este comportamiento no estándar, 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 método tiene una desventaja: si usamos un conjunto variado de widgets en el formulario (por ejemplo

QComboBox y QSpinBox) además, tendríamos que hacer subclases de ellos para que tengan el mismo

comportamiento que el MiLineEdit. Una solución 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 creación de un filtro de eventos conlleva dos pasos:

1. Registrar el objeto que monitorea con el objeto de destino, llamando a la función

installEventFilter() en el objeto destino.

2. Controlar los eventos emitidos por el objeto destino con la función eventFilter() del objeto

monitor.

El constructor de la clase es un buen lugar para registrar los filtros de eventos u objetos de monitoreo, como

también 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 serán enviados primero a la función

eventFilter() de DialogoInfoCliente, antes que a su destino original.

Aquí vemos la implementación de la función 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);

36 7. Procesamiento de Eventos

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 presión 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 tabulación 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 presión 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 específico.

Re implementando los manejadores de eventos tales como mousePressEvent(), keyPressEvent()

y paintEvent() está, por mucho, la manera mas común de procesar eventos. Ya hemos visto varios

ejemplos a lo largo del libro.

2. Podemos re implementar QObject::event().

Con esta técnica 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. También es usada para manejar tipos raros de eventos para los que no existen controladores

específicos (por ejemplo, el evento QEvent::HoverEnter). Después que re implementemos event(),

debemos llamar a la función event() de la clase base para que se encargue de los casos que no

controlamos explícitamente.

3. Podemos instalar filtros de eventos en un solo QObject.

Una vez que el objeto haya sido registrado con la función installEventFilter(), todos los eventos

destinados a ese objeto serán enviados primero a la función eventFilter() del objeto monitor. Si

instalamos varios filtros en el mismo objeto, estos serán procesados por turnos, comenzando por el instalado

más 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 aplicación será enviado primero a la función

eventFilter(), antes que a cualquier otro filtro de eventos. Esta técnica suele ser útil en el proceso de

depuración o para controlar eventos del ratón 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 función es la

única manera de tener acceso a los eventos antes de que cualquier filtro de eventos los llegue a procesar. Los

37 7. Procesamiento de Eventos

filtros de eventos son generalmente más útiles, porque podemos tener cualquier cantidad de filtros

concurrentes, pero solo tendremos una función 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 jerarquía.

Figura 7.2. Propagación de evento en un dialogo

La Figura 7.2 muestra cómo, en un dialogo, un evento generado por la presión 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. Después de esto, se ejecuta el ciclo principal de eventos en

donde constantemente se verifica si ha ocurrido algún evento y de ser así, los envía a los objetos

(QObjects) de la aplicación.

Mientras un evento está siendo procesado, los eventos adicionales que se generen serán 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 aplicación

guarda un archivo no será procesado hasta que no se termine de guardar el archivo. Durante este tiempo la

aplicación no atenderá ningún requerimiento, ni siquiera la solicitud de re dibujado realizada por parte del

sistema de ventanas.

Una solución para este caso seria usar varios hilos: uno para la interfaz grafica de la aplicación y otro para el

proceso que requiera demasiado tiempo de operación (como el guardado de archivos). De esta manera, la

interfaz de usuario podrá recibir requerimientos mientras el archivo se guarda. Veremos cómo se hace esto

en el Capítulo 18.

Una solución más simple es realizar llamadas frecuentes a QApplication::processEvents() en el

código donde realizamos el proceso intensivo (p. e., en el código de guardado del archivo). La función

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 más que un

ciclo mientras (while) envolviendo llamadas periódicas a la función processEvents().

Aquí presentamos un ejemplo de esta técnica, basándonos en el código de guardado de archivo de la

aplicación Hoja de Cálculo:

38 7. Procesamiento de Eventos

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 podría cerrar la ventana principal mientras la

aplicación aun se encuentra realizando el guardado del archivo o volver a activar otra vez la misma acción

desde el menú Archivo|Guardar, obteniendo un comportamiento indefinido e inesperado como resultado. La

solución más fácil 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 ratón.

Mientras se está ejecutando una operación larga, es común 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 aplicación y un botón que permite abortar la operación en cualquier

momento. Este sería el código para guardar un archivo de la aplicación Hoja de Cálculo 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;

}

39 7. Procesamiento de Eventos

Creamos un QProgressDialog con la variable CantidadFilas como el número total de pasos a

ejecutar. Entonces, por cada paso, llamamos a setValue() para actualizar la barra de progreso. El objeto

QProgressDialog automáticamente 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 botón Cancelar). Si el

usuario decide cancelar, abortamos el guardado y borramos el archivo.

No llamamos a la función show() del QProgressDialog porque este lo hace por sí mismo.

QProgressDialog puede detectar si la operación resultará demasiado corta, ya sea porque el archivo es

pequeño o el equipo es demasiado rápido, y no se muestra.

Aparte de las técnicas de hilos múltiples, 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 aplicación esté inactiva. Esto puede funcionar si el

procesamiento puede ser interrumpido sin perjuicio alguno y reanudado, ya que no sabemos cuánto tiempo

estará inactiva la aplicación.

En Qt, este técnica 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 función timerEvent() implementando esta técnica:

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 más eventos pendientes.

40 8. Gráficos En 2 y 3 Dimensiones

8. Gráficos En 2 y 3 Dimensiones

Dibujando con QPainter

Transformaciones

Renderizado de Alta Calidad con QImage

Impresión

Gráficos con Open GL

El motor para generar gráficos en dos dimensiones (2D) de Qt se basa principalmente en la clase

QPainter. Esta clase puede dibujar figuras geométricas básicas (puntos, líneas, rectángulos, elipses, arcos,

líneas curvas, segmentos circulares, polígonos y curvas Bezier), así como también imágenes y texto. Aun

más, QPainter soporta características avanzadas tales como antialiasing (para textos y bordes de figuras),

transparencias, rellenos con degradados y trazados vectoriales. También soporta transformaciones, las cuales

posibilitan el dibujo de gráficos 2D independientes de la resolución 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. También puede ser

usada en conjunción con QPrinter para realizar impresiones o generar archivos PDF. Esto nos posibilita, a

menudo, usar el mismo código ya sea para mostrar datos por pantalla o producir reportes impresos.

Una alternativa a QPainter es usar OpenGL. Esta es una librería estándar para generación de gráficos en

dos o tres dimensiones. El módulo QtOpenGL hace que sea fácil integrar código OpenGL en aplicaciones

realizadas con Qt.

Dibujando con QPainter

Para comenzar a trabajar sobre un dispositivo de dibujo (típicamente un widget), simplemente creamos un

objeto QPainter pasándole 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 más 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 unión.

41 8. Gráficos En 2 y 3 Dimensiones

brush(): propiedad de tipo QBrush. Es un patrón usado para rellenar figuras geométricas.

Normalmente consta de un color y un estilo, pero también 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 tamaño de punto.

Estas propiedades pueden ser modificadas en cualquier momento usando las funciones setPen(),

setBrush() y setFont().

Figura 8.1. Las funciones draw…() más usadas de QPainter

42 8. Gráficos En 2 y 3 Dimensiones

Figura 8.2. Estilos de cubierta y de unión

Figura 8.3. Estilos de pluma

Figura 8.4. Estilos de pinceles predeterminados

Figura 8.5. Ejemplos de figuras geométricas

43 8. Gráficos En 2 y 3 Dimensiones

Veamos algunos ejemplos prácticos. Este es el código 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 función setRenderHint() permite activar el antialiasing, haciendo que QPainter use distintas

intensidades de color al dibujar los bordes de las figuras para reducir la distorsión 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 característica.

Este es el código 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 porción.

Este es el código 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 conexión de elementos

gráficos básicos: líneas rectas, círculos, polígonos, arcos, curvas Bezier (cuadráticas o cubicas) y otras

figuras vectoriales. Las figuras vectoriales son la primitiva de dibujo definitiva en el sentido de que cualquier

otra figura o combinación 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 interpolación entre dos o más 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, cónico y radial. El ejemplo Temporizador de Horno presentado

en la siguiente sección combina los tres tipos en un solo widget para lograr un aspecto real.

44 8. Gráficos En 2 y 3 Dimensiones

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 código:

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 serán

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 más el radio forman un circulo. Los colores se

esparcirán desde el punto focal (el cual puede ser cualquier punto dentro del círculo) hacia el

exterior.

Degradado cónico: esta definido por un punto central (x, y) y un ángulo α. Los colores se esparcirán

alrededor del punto central siguiendo la dirección de las agujas del reloj.

Hasta ahora solo hemos usado solo tres propiedades de QPainter (pen(), brush() y font()).

QPainter posee más 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 imágenes 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 patrón 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á ningún efecto.

45 8. Gráficos En 2 y 3 Dimensiones

viewport(), window() y worldTransform(): estas tres propiedades determinan cómo

QPainter mapea las coordenadas lógicas a las coordenadas físicas del dispositivo. Por defecto,

estas coinciden. El sistema de coordenadas será analizado en la próxima sección.

compositionMode(): es una enumeración que especifica cómo los nuevos pixeles dibujados

deberán interactúan con los ya existentes. Por defecto está en "source over", en donde los pixeles se

dibujan encima de los existentes. Esta característica no está soportada por todos los dispositivos y la

veremos más adelante en este capítulo.

En cualquier momento, podemos guardar el estado de nuestro objeto QPainter en una pila interna

llamando a la función save() y restablecerlo más tarde con la función restore(). Esto puede ser útil si

queremos cambiar temporalmente alguna configuración, como veremos en la próxima sección.

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 tamaño 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 dirección, obteniendo como resultado un

pixel dibujado en (100.5, 100.5).

Esto les podrá parecer bastante académico al principio, pero tiene consecuencias importantes en la práctica.

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 además 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 impresión 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 líneas, rectángulo o elipses se aplica una regla parecida. La Figura 8.7

muestra el resultado de cómo varía la llamada a drawRect(2, 2, 6, 5) de acuerdo al ancho del lápiz,

con el antialiasing desactivado. Es importante observar que un rectángulo de 6x5 dibujado con un lápiz 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 gráficos vectoriales realmente escalables e

independientes de la resolución.

Figura 8.7. Dibujando un rectángulo 6x5 sin atialiasing

Ahora que hemos entendido cómo 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, están muy relacionados. El viewport es un rectángulo arbitrario

especificado en coordenadas físicas.

Window: es el mismo rectángulo que "viewport" solo que especificado en coordenadas lógicas.

46 8. Gráficos En 2 y 3 Dimensiones

Cuando damos una orden para realizar un dibujo especificamos la ubicación de los puntos en coordenadas

lógicas, estas son transformadas en coordenadas físicas de manera algebraica, basándose en la configuración

del viewport de la ventana actual.

Por defecto, "viewport" y "window" se establecen al rectángulo del dispositivo, coincidiendo el sistema de

coordenadas físico con el lógico. Por ejemplo, si tenemos un widget de 320x200, el "viewport" y el

"window" tendrán este tamaño.

La conjunción entre "viewport" y "window" hacen posible la realización de dibujos independiente del

tamaño o de la resolución del dispositivo destino. Por ejemplo, si queremos que las coordenadas lógicas 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 lógica (-50,-50) ahora corresponda a la

coordenada física (0,0) y la coordenada lógica (50,50) se corresponda a la coordenada física (320,200). En

este ejemplo no hemos cambiado el "viewport".

Figura 8.8. Convirtiendo coordenadas lógicas a coordenadas físicas

Ahora nos dedicaremos a la "world matrix". Esta es una matriz de transformación que se aplica

adicionalmente a la conversión 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º, podríamos usar este

código:

QMatrix matriz;

matriz.rotate(45.0);

painter.setMatrix(matriz);

painter.drawText(rect, Qt::AlignCenter, tr("Ingresos"));

La coordenada lógica que le pasamos a drawText() primero es transformada por la "world matrix", y

luego mapeada a coordenadas físicas usando las configuraciones window-viewport.

Si especificamos varias transformaciones, estas serán aplicadas en el orden en que las fuimos creando. Por

ejemplo: si queremos usar el punto (10,20) como punto pivote de rotación, podemos primero mover el

"window", realizar la rotación y luego volver el "window" a su posición 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 más simple de realizar transformaciones es usar las siguientes funciones de QPainter:

translate(), scale(), rotate() y shear():

painter.translate(-10.0, -20.0);

47 8. Gráficos En 2 y 3 Dimensiones

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 transformación, 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 código 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

común la incorporación de relojes en los hornos. El usuario puede hacer click sobre una marca para

establecer la duración del temporizador. La rueda girará automáticamente hasta alcanzar el cero, en este

punto, emitirá la señal 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 double GradosPorMinuto = 7.0;

const double GradosPorSegundo = GradosPorMinuto / 60;

const int MaxMinutos = 45;

const int MaxSegundos = MaxMinutos * 60;

const int IntervaloActualizacion = 1;

Comenzamos definiendo unas cuantas constantes que controlarán la apariencia del widget.

48 8. Gráficos En 2 y 3 Dimensiones

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 señal tiempoTerminado() cuando el contador llega a

0. Como este último solo necesita emitir la señal una vez, establecemos setSingleShot(true); ya que

por defecto los temporizadores emiten señales repetidamente hasta que son detenidos o destruidos. La ultima

llamada a connect() es una optimización 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 función setDuracion() establece la duración del temporizador a un número de segundos dado.

Calculamos la hora de finalización agregando la duración a la hora actual (obtenida a través 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 ocasionaría si estableciéramos el tiempo actual antes de medianoche y que el

conteo finalizara después de esta.

int TempHorno::duracion() const

{

int segs = QDateTime::currentDateTime().secsTo(horaFin);

if (segs < 0)

segs = 0;

return segs;

}

49 8. Gráficos En 2 y 3 Dimensiones

La función 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 más cercana utilizando una simple,

pero eficaz, formula matemática, y usamos el resultado obtenido como la nueva duración 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 función paintEvent(), configuramos un "viewport" del tamaño del cuadrado más grande que entre

en el widget, y configuramos un "window" de tamaño 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. Después de esto llamamos a la función dibujar() que se encargara de dibujar el widget.

Si no establecemos el "viewport" a un cuadrado, el widget se transformaría en una elipse cuando, por

cambios de tamaño, este no tenga el mismo alto que ancho. Para evitar esta deformación, debemos establecer

el "vieport" y el "window" con la misma relación de aspecto.

Figura 8.10. El widget TempHomo en tres tamaños distintos

50 8. Gráficos En 2 y 3 Dimensiones

Ahora nos centraremos en el código 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 pequeño triángulo que marca la posición cero en la parte superior del widget. El

triángulo es definido por tres coordenadas fijas y dibujado por medio de la función drawPolygon().

Aquí vemos que una de las ventajas del mecanismo "window–viewport" es que, por más que dibujemos el

triángulo en coordenadas fijas, vamos a obtener un buen resultado cuando se cambie el tamaño 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 círculo exterior y lo pintamos con un degradado cónico. 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) {

51 8. Gráficos En 2 y 3 Dimensiones

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 función 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 después de la rotación, ya que su inclinación depende del ángulo

de rotación.

En el ciclo for, dibujamos las marcas sobre el borde del circulo exterior y los números para cada múltiplo de

5 minutos. El texto se dibuja en un rectángulo invisible debajo de la marca. Al final de cada iteración,

rotamos el lienzo 7º en contra de las agujas del reloj (lo que corresponde a un minuto). La próxima vez que

dibujemos una marca, estará en una posición distinta alrededor del circulo, aun cuando le pasemos las

mismas coordenadas a las funciones drawLine() y drawText().

El código del ciclo for tiene un pequeño defecto, el cual podría volverse aparente si realizáramos demasiadas

iteraciones. Cada vez que llamamos a rotate(), se genera una nueva "world matrix" mediante una

rotación. Al irse acumulando los errores de redondeo asociados a las operaciones aritméticas en punto

flotante, producen una "world matrix" inexacta. Una manera de evitar esto es reescribir el código usando las

funciones save() y restore() para guardar y restablecer la matriz de transformación original en cada

iteración.

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 círculo. Pero todavía tendríamos la necesidad de usar las

operaciones de translación y rotación 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

precisión. 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

comunicación con el servidor X se mantiene al mínimo; 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:

52 8. Gráficos En 2 y 3 Dimensiones

En X11, algunas características, como son antialiasing y soporte para coordenadas fraccionales,

están disponibles solo si la extensión X Render está presente en el servidor X.

En MacOs X, el motor gráfico de antialiasing usa diferentes algoritmos que X11 y Windows para

dibujar polígonos, obteniendo resultados ligeramente diferentes.

Cuando la precisión es más importante que la eficiencia, podemos dibujar en un QImage y copiar el

resultado a la pantalla. Esta técnica siempre usa el motor de dibujo interno de Qt, obteniendo resultados

idénticos en todas las plataformas. La única restricción 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 idéntico 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 extensión X Render. El controlador original del evento

paintEvent(), el cual se basa en X Render para lograr el antialiasing, podría 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 continuación mostramos cómo quedaría el código anterior si utilizamos el motor gráfico 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 tamaño 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 método produce resultados de alta calidad idénticos en todas las plataformas, con la excepción del

dibujado de las fuentes, que depende de las fuentes instaladas en el sistema.

Una característica realmente poderosa del motor gráfico de Qt es el soporte para modos de composición.

Cada uno de estos modos especifica cómo los pixeles de origen y destino serán 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

53 8. Gráficos En 2 y 3 Dimensiones

transparencia. La Figura 8.11 muestra el resultado de dibujar una mariposa semitransparente sobre un patrón

verificador utilizando cada uno de los distintos modos.

Los modos de composición se establecen por medio de QPainter::setCompositionMode(). Por

ejemplo, esta seria la forma de crear un objeto QImage que contenga una operación XOR entre la mariposa

y el patrón verificador:

QImage imagenResultado = imagenPatron;

QPainter painter(&imagenResultado);

painter.setCompositionMode(QPainter::CompositionMode_Xor);

painter.drawImage(0, 0, imagenMariposa);

Figura 8.11. Modos de composición de QPainter

Una característica a tener en cuenta de la operación QImage::CompositionMode_Xor es que también

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).

Impresión

La impresión 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

impresión.

3) Crear un objeto QPainter que opere con el objeto QPrinter.

4) Dibujar sobre la página usando el objeto QPainter.

5) Llamar a la función QPrinter::newPage() para pasar a la siguiente pagina.

6) Repetir el paso 4 y el 5 hasta que se hayan impreso todas las páginas.

En Windows y MacOs X, QPrinter usa los drivers de impresión del sistema. En Unix, se genera un

PostScript y se envía a lp o lpr (o el programa establecido mediante

QPrinter::setPrintProgram()). El objeto QPrinter también puede ser usado para generar

archivos PDF con solo llamar a setOutputFormat(QPrinter::PdfFormat).

54 8. Gráficos En 2 y 3 Dimensiones

Figura 8.12. Imprimiendo un objeto QImage

Comenzaremos con ejemplos simples que impriman solo en una hoja. El primer ejemplo muestra cómo

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 podríamos haber creado el objeto QPrinter local al método

imprimirImagen(), pero no tendríamos forma de recordar las preferencias del usuario entre distintas

impresiones.

55 8. Gráficos En 2 y 3 Dimensiones

Creamos un objeto QPrintDialog y lo mostramos por medio de la función exec(). Esta devuelve true

si el usuario presiona el botón Aceptar del dialogo, sino devuelve false. Después de la llamada a

exec(), el objeto QPrinter está listo para usarse, aunque también es posible realizar una impresión sin

usar un QPrintDialog, solamente establecemos las preferencias de impresión por medio de las funciones

miembro del objeto QPrinter.

A continuación, creamos un objeto QPainter que nos permitirá dibujar sobre el objeto QPrinter.

Establecemos el "viewport" a un rectángulo con la misma relación de aspecto que la imagen y el "window"

al tamaño de la imagen, luego dibujamos la imagen en la posición (0,0).

Predeterminadamente, el "window" de QPainter es inicializado de manera tal que la impresora parezca

tener una resolución similar a la pantalla (usualmente un valor entre 72 y 100 puntos por pulgadas), haciendo

que sea fácil reutilizar el código de dibujado del widget para imprimir. En el ejemplo, esto no importa porque

hemos establecido nuestro propio "window".

La impresión de items que no sobrepasan una página es muy simple, pero la mayoría de las aplicaciones

necesitan imprimir datos en varias páginas. Para esto, se dibuja el contenido de una página por vez

intercalando un llamado a newPage() cada vez que se quiera pasar a una nueva. El problema que surge es

que debemos determinar cuánta información va a ser impresa en cada página. En Qt hay dos enfoques

principales para manejar la impresión de documentos de varias páginas:

Podemos convertir los datos a HTML e imprimirlos mediante el motor de texto enriquecido de Qt

(QTextDocument).

Podemos realizar el dibujo de las páginas manualmente.

Revisaremos cada enfoque por turnos. Como un ejemplo, imprimiremos una guía de flores compuesta por

una lista de nombres y descripciones. Cada entrada es almacenada como una cadena de caracteres en formato

"nombre: descripción", por ejemplo:

Miltonopsis santanae: Una especie de orquídea muy peligrosa.

Ya que cada ítem de la guía de flores es una cadena de caracteres, usaremos una QStringList para

representarla. Esta es la función 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 función

imprimeHtml():

void VentanaImpresion::imprimeHtml(const QString &html)

{

QPrintDialog dialogoImpresion(&impresora, this);

56 8. Gráficos En 2 y 3 Dimensiones

if (dialogoImpresion.exec()) {

QPainter painter(&impresora);

QTextDocument documentoTexto;

documentoTexto.setHtml(html);

documentoTexto.print(&impresora);

}

}

La función imprimeHtml() muestra un QPrintDialog y se encarga de imprimir el documento. La

misma puede ser reutilizada "tal como está" en cualquier otra aplicación para imprimir cualquier página

HTML.

Figura 8.13. Imprimiendo una guía 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 más control,

podemos establecer el diseño y dibujo de la página a mano. Veamos cómo podemos usar este enfoque para

imprimir la guía 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);

}

}

Después de configurar la impresora y crear el objeto QPainter, llamamos a la función de soporte

paginar() para que determine las entradas que deberían aparecer en cada pagina. El resultado de esto es

una lista de objetos QStringLists que contiene los datos a imprimir en cada página. A esta lista la

pasamos a la función imprimirPaginas().

57 8. Gráficos En 2 y 3 Dimensiones

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 página; D, C y E

en la segunda y F en la tercera. La lista de páginas debería contener una lista con los objetos [A, B] en la

posición 0, la lista [C, D, E] en la posición 1 y la lista [F ] en la posición 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 función paginar() distribuye las entradas en cada página. Esta se basa en la función

altoEntrada(), la cual calcula el alto de una entrada. También toma en cuenta los espacios verticales

vacíos al principio y al final página, cuyo tamaño está almacenado en la variable espacioGrande.

Iteramos sobre las entradas y las vamos agregando a la página actual hasta que lleguemos a una entrada que

no entre en el espacio en blanco de la página, entonces anexamos la página actual a la lista de páginas 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 función 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 guía de flores y la

representación de la constantes espacioChico y espacioMediano.

58 8. Gráficos En 2 y 3 Dimensiones

Figura 8.14. Layout de un ítem de la guía 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 función imprimirPaginas() es enviar cada pagina a la impresora en el orden y cantidad de

veces correcta. Usando la clase QPrintDialog, el usuario podría requerir la impresión de varias copias,

un rango de páginas o que se imprima en orden inverso. Es nuestra responsabilidad respetar estas opciones o

desactivarlas por medio de QPrintDialog::setEnabledOptions().

59 8. Gráficos En 2 y 3 Dimensiones

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 número de página de inicio y fin del

rango de impresión seleccionado por el usuario o cero si no se escogió un rango. Como el índice de la lista

de páginas esta basado en cero, tenemos que restar uno a los valores de las páginas seleccionadas.

Después imprimimos cada página. El primer ciclo itera las veces necesarias para producir la cantidad de

copias requeridas por el usuario. La mayoría de los drivers de impresoras soportan copias múltiples, 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 aplicación la encargada de

imprimirlas (en la impresión de QImage que vimos anteriormente en este capitulo, ignoramos la cantidad de

copias por cuestiones de simplicidad).

Figura 8.15. Imprimiendo una guía de flores usando un objeto QPainter

El ciclo for interno itera a través de las páginas. Si la página actual no es la primera, llamamos a

newPage() para limpiar la página anterior y empezar a trabajar con una nueva. Mediante la función

imprimirPagina() cada página 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);

60 8. Gráficos En 2 y 3 Dimensiones

painter->drawText(painter->window(),

Qt::AlignHCenter | Qt::AlignBottom,

QString::number(numeroPagina));

}

La función imprimirPagina() recorre la guía de flores e imprime cada entrada mediante dos llamadas a

la función imprimirRecuadro(); una para el título y otra para la descripción. También se encarga de

dibujar el número de cada página.

Figura 8.16. Layout de página de la guía 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 función imprimirRecuadro() dibuja el borde de un recuadro y el texto dentro de este.

61 8. Gráficos En 2 y 3 Dimensiones

Gráficos con OpenGL

OpenGL es una API estándar para la generación de gráficos en dos y tres dimensiones. Las aplicaciones

realizadas con Qt pueden dibujar gráficos en 3D usando el módulo QtOpenGL, el cual se basa en las librerías

OpenGL instaladas en el sistema. En esta sección se asume que el lector tiene conocimientos previos sobre la

utilización 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 aplicación Tetraedro

Generar gráficos mediante OpenGL en un programa realizado con Qt es sencillo. Debemos subclasificar la

clase QGLWidget, reimplementar algunas funciones virtuales y enlazar la aplicación con la librería

OpenGL (mediante el módulo 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 cómo trabaja, revisaremos el código de la aplicación Tetraedro mostrada en la Figura 8.17. La

aplicación 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 ratón mientras mantiene presionado el botón 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];

62 8. Gráficos En 2 y 3 Dimensiones

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 función QGLWidget::setFormat() para especificar las características

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 función 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 código está compuesto de llamadas a funciones de OpenGL, excepto por qglClearColor(). Si

queremos mantener todo en OpenGL estándar, podríamos 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 función resizeGL() es llamada antes que se llame por primera vez a paintGL(), pero después de la

llamada a initializeGL(). Ésta también es llamada cada vez que el widget cambia de tamaño. Este es

el lugar en donde podemos configurar el "viewport" de OpenGL, las proyecciones y cualquier otro valor que

dependa del tamaño del widget.

void Tetraedro::paintGL()

{

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

dibujar();

}

La función paintGL() es llamada cada vez que el widget necesite redibujarse. Esto es muy parecido a usar

QWidget::paintEvent(). El dibujo es realizado por la función privada dibujar().

63 8. Gráficos En 2 y 3 Dimensiones

void Tetraedro::dibujar()

{

static const GLfloat P1[3] = { 0.0, -1.0, +2.0 };

static const GLfloat P2[3] = { +1.73205081, -1.0, -1.0 };

static const GLfloat P3[3] = { -1.73205081, -1.0, -1.0 };

static const GLfloat P4[3] = { 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 función 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 código esta formado por llamadas a funciones

de OpenGL, excepto por qglColor(). En vez de ésta, podríamos 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();

}

64 8. Gráficos En 2 y 3 Dimensiones

Las funciones mousePressEvent() y mouseMoveEvent() se reimplementan de QWidget para

permitirle al usuario rotar la figura. El botón izquierdo del ratón permite rotar la figura sobre el eje x y el eje

y, mientras que el botón derecho lo hace sobre el eje x y el eje z.

Después de modificar cualquiera de las variables rotación, rotación y rotación llamamos a la función

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 función mouseDoubleClickEvent() responde a la realización de un doble click sobre la figura y

permite establecer el color de una cara de la misma. Por medio de caraEnPosicion() determinamos

cuál 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 función caraEnPosicion() devuelve el numero de cara que se encuentra en una posición

determinada o -1 si o hay nada en dicha posición. El código 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 selección de OpenGL y así poder devolver el número de cara.

65 8. Gráficos En 2 y 3 Dimensiones

É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 módulo QtOpenGL de la aplicación con las librerías de OpenGL, necesitamos agregar la

siguiente entrada al archivo .pro

QT += opengl

Con esto completamos la aplicación Tetraedro. Para obtener mas información sobre el módulo QtOpenGL,

consulte la documentación de referencia de QGLWidget, QGLFormat, QGLContext, QGLColormap y

QGLPixelBuffer.

66 9. Arrastrar y Soltar

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 aplicación o entre

diferentes aplicaciones. Es, además, a menudo provisto como soporte del portapapeles para mover y copiar

datos.

En este capítulo, veremos cómo agregar soporte a una aplicación para arrastrar y soltar y cómo manejar

formatos personalizados. A continuación vamos a mostrar la forma de reutilizar el código de arrastrar y

soltar para añadir soporte al portapapeles. Esta reutilización de código 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 liberación. Los widgets de Qt pueden servir como

sitios de arrastre, como sitios para soltar, o como ambos.

Nuestro primer ejemplo muestra cómo hacer una aplicación que acepte un arrastre iniciado por otra

aplicación. La aplicación 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 aplicación, la aplicación carga el archivo dentro del QTextEdit.

Aquí está la definición 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 propósito del ejemplo es mostrar cómo arrastrar y soltar, gran parte de la funcionalidad que esperaríamos

que esté en una clase de ventana principal se ha omitido.

67 9. Arrastrar y Soltar

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 habilitándola 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 función 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 legítimo para soltar.

Aquí queremos que al usuario se le permita arrastrar archivos y nada más. 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 sólo

arrastran un archivo a la vez, pero es posible que arrastren varios archivos mediante una selección. Si hay

más de un URL, o si la URL no es un nombre de archivo local, retornamos inmediatamente.

QWidget también proporciona las funciones dragMoveEvent() y dragLeaveEvent(), pero la

mayoría de las aplicaciones no necesitan ser reimplementadas.

El segundo ejemplo muestra cómo 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 aplicación Selector de

Proyecto (Project Chooser) que se muestra en la Figura 9.1.

68 9. Arrastrar y Soltar

Figura 9.1. La aplicación Selector de Proyecto

La aplicación 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 código de arrastrar y soltar está ubicado en la subclase QListWidget. Aquí esta la definición 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 opción 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 botón izquierdo del ratón, almacenamos la posición del ratón en la variable

privada startPos. Llamamos a la implementación de mousePressEvent() perteneciente a

69 9. Arrastrar y Soltar

QListWidget para asegurar que QListWidget tenga la oportunidad de procesar los eventos del ratón

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 ratón mientras mantiene pulsado el botón izquierdo del ratón, se

considera que se ha iniciado un arrastre. Calculamos la distancia entre la posición actual del ratón y la

posicón en la que se ha pulsado el botón izquierdo del ratón. Si la distancia es más 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 sólo 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 más comunes (imágenes, 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 operación de arrastre y bloquea hasta que el usuario suelte o

cancele el arrastre. Se necesita una combinación de las "acciones de arrastre" soportadas como argumento

(Qt::CopyAction, Qt::MoveAction y Qt::LinkAction) y retorna la acción de arrastre que fue

ejecutada (o Qt::IgnoreAction si ninguna fue ejecutada). La ejecución de una acción 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 liberación. Despues de la llamada a start(), Qt toma posesión 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();

}

}

70 9. Arrastrar y Soltar

El widget ProjectListWidget no sólo origina el arrastre, tambien acepta el arrastre si proviene de otro

ProjectListWidget en la misma aplicación. QDragEnterEvent::source() retorna un puntero al

widget que inició el arrastre si ese widget es parte de la misma aplicación; 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 acción

como una acción 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 idéntico al que hicimos en dragEnterEvent(). Es necesario

debido a que necesitamos reemplazar la implementación 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 "acción 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 aplicación, 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, imágenes, 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 utilizando

QMimeData::setData() y extraerlo más adelante con el QMimeData::data().

2. Podemos hacer una subclase QMimeData y reimplementar los métodos formats() y

retrieveData() para manejar nuestros tipos de datos personalizados.

3. Para las operaciones de arrastrar y soltar dentro de una sola aplicación, podemos hacer una subclase

de QMimeData y almacenar los datos con cualquier estructura de datos que queramos.

71 9. Arrastrar y Soltar

La primera opción 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 aplicación innecesariamente. Las otras dos opciones pueden evitar o minimizar estos problemas.

Nos dan un control completo y se pueden utilizar juntas.

Para mostrar cómo estos métodos funcionan, vamos a mostrar cómo 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 opción, el método para iniciar un arrastre luciría 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 función privada startDrag() se llama desde mouseMoveEvent() para empezar a arrastrar una

selección 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 código para selectionAsString() es más o menos lo mismo que la

función Spreadsheet::copy() vista en el Capítulo 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;

72 9. Arrastrar y Soltar

}

Las funciones toCsv() y toHTML() convierten una cadena de "etiquetas y saltos de línea" 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 conversión se realiza en la forma más 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, también tenemos que llamar a setAcceptDrops(true) y

setSelectionMode(ContiguousSelection) en el constructor de MyTableWidget.

A continuación, 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 definición 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);

73 9. Arrastrar y Soltar

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 están 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 función 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 práctica poner los "mejores" formatos

primero. Las aplicaciones que admiten muchos formatos algunas veces usarán 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 función retrieveData() devuelve los datos para un tipo MIME dado como QVariant. El valor del

parámetro 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 términos de retrieveData().

El parámetro 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) {

74 9. Arrastrar y Soltar

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 función dropEvent() es similar a la que había anteriormente en esta sección, pero esta vez la

optimizamos comprobando primero si podemos convertir con seguridad el objeto QMimeData a un

TableMimeData. Si la instrucción qobject_cast <T>() funciona, significa que el arrastre se originó

por un objeto de tipo MyTableWidget en la misma aplicación, y podemos acceder directamente a los

datos de tabla en vez de ir a través de la API de QMimeData. Si la instrucción falla, extraemos los datos de

la forma estándar.

En este ejemplo, codificamos el texto CSV con la codificación UTF-8. Si queremos estar seguros de utilizar

la codificación correcta, podríamos utilizar el parámetro charset del tipo MIME text/plain para

especificar una codificación explícita. Aquí están 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 mayoría 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 ningún código adicional se requiera.

Al escribir nuestras propias clases, podemos acceder al portapapeles a través

QApplication::clipboard(), que devuelve un puntero al objeto QClipboard de la aplicación. El

manejo del portapapeles del sistema es fácil: 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 aplicación de hoja de cálculo en el

Capítulo 4.

Para algunas aplicaciones, la funcionalidad integrada podría no ser suficiente. Por ejemplo, podríamos

proporcionar datos que no son sólo texto o imagen, o queremos proporcionar datos en muchos formatos

diferentes para la máxima interoperabilidad con otras aplicaciones. La cuestión es muy similar a lo que nos

encontramos antes con arrastrar y soltar, y la respuesta también es similar: Podemos hacer una subclase de

QMimeData y reimplementar unas pocas funciones virtuales.

Si nuestra aplicación es compatible con arrastrar y soltar a través de una subclase de QMimeData,

simplemente podemos volver a utilizar la subclase QMimeData y ponerla en el portapapeles con la función

setMimeData(). Para recuperar los datos, podemos llamar a mimeData() en el portapapeles.

En X11, por lo general es posible pegar una selección haciendo clic en el botón central del ratón (para uno de

tres botones). Esto se hace utilizando un portapapeles de "selección" 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

75 9. Arrastrar y Soltar

ejemplo, aquí está la forma en que se reimplementaría mouseReleaseEvent() en un editor de texto para

soportar el pegado con el botón central del ratón:

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 función 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

señal QClipboard::dataChanged() a un slot personalizado.

76 10. Clases para Visualizar Elementos (Clases Item View)

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 inglés).

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 podía realizar las operaciones de

búsqueda y edición directamente sobre los elementos contenidos en este, y en algún punto los cambios eran

enviados al origen de datos. Aunque esta técnica 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 técnica flexible para visualizar grandes cantidades de datos: el modelo

vista-controlador (MVC por sus siglas en ingles: Model View Controller). En esta técnica, 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

pequeña parte de ellos a la vez, de manera tal que el modelo únicamente tiene que recuperar una pequeña

porción de datos del origen. El controlador es un mediador entre el usuario y la vista, convirtiendo las

acciones del usuario en requerimientos de navegación o edición, 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 clásico. Pero, en lugar del controlador, se usa una abstracción ligeramente diferente: el

delegado. El delegado es usado para proveer un control fino sobre el dibujado y edición de los elementos. Qt

proporciona un delegado por defecto para cada tipo de vista. Esto es suficiente para la mayoría 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 manipulación de grandes cantidades de datos sea rápida y con un

consumo de memoria menor que el proceso de cargar todos los datos a la vez. Y mediante el registro de un

77 10. Clases para Visualizar Elementos (Clases Item View)

modelo en dos o más vistas, podemos dar al usuario la oportunidad de ver e interactuar con los mismos datos

de diferentes maneras, con una pequeña sobrecarga. Qt se encarga de mantener las vistas sincronizadas,

reflejando los cambios realizados en una a las demás. 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

continuarán comportándose correctamente.

Figura 10.2. Un modelo puede proporcionar datos a múltiples vistas

En muchas situaciones, solo necesitamos presentar una cantidad relativamente pequeña 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 múltiples 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 opción. En estos casos,

podemos usar una conjunción 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 más 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 técnica en el

Capítulo 4 cuando subclasificamos QTableWidget y QTableWidgetItem para implementar la

funcionalidad de la hoja de cálculo.

En esta sección 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 símbolo 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);

•••

};

78 10. Clases para Visualizar Elementos (Clases Item View)

Figura 10.3. La aplicación 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 función 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

continuación creamos un QListWidget. Mediante un iterador recorremos los elementos de la lista de

símbolos 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.

Después establecemos el icono del QListWidgetItem y llamamos a setData() para agregarle un

identificador al mismo. La función 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 más 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 además de estos existen otros roles. También podemos definir roles personales especificando un valor

numérico mayor o igual a Qt::UserRole. En nuestro ejemplo usamos Qt::UserRole para almacenar

el identificador de cada elemento.

79 10. Clases para Visualizar Elementos (Clases Item View)

Omitimos el código 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 función done() la reimplementamos de la clase QDialog, y es llamada cuando el usuario presiona el

botón OK o el botón Cancelar. Si se presiona OK, obtenemos el id del elemento seleccionado por medio de

la función data(). Si estuviéramos interesados en el texto del elemento, podríamos obtenerlo por medio de

item->data(Qt::DisplayRole).toString() o, lo que es más conveniente, item->text().

Por defecto, la clase QListWidget presenta datos en modo de solo lectura. Si queremos que el usuario

edite los datos mostrados, podríamos establecer el disparador de edición 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, también podríamos proveer un botón Editar (y por supuesto Agregar y

Eliminar) y conectarlos a los slots que manejarían las operaciones de edición programáticamente.

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 aplicación Configurador de Coordenadas

Como en el ejemplo anterior, solo nos centraremos en el código 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) {

80 10. Clases para Visualizar Elementos (Clases Item View)

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 función 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

parámetro. Por defecto, QTableWidget etiqueta los encabezados verticales con el número de fila,

comenzando por el número 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 función setItem(), a la cual hay que indicarle la posición de inserción (número de fila y de

columna).

La clase QTableWidget siempre permite la edición 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 serán automáticamente reflejados en el QTableWidgetItem apropiado. Si queremos

prevenir la edición 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 botón Agregar Fila. Para eso

utilizamos la función insertRow(). Si el usuario intenta modificar alguna celda de la nueva fila,

QTableWidget automáticamente creará un nuevo objeto QTableWidgetItem vacío.

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));

81 10. Clases para Visualizar Elementos (Clases Item View)

}

}

QDialog::done(result);

}

Por último, cuando el usuario presiona el botón 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 aplicación que nos ilustrarán en el uso de la

clase QTreeWidget. Esta, predeterminadamente, presenta datos de solo lectura.

Figura 10.5. La aplicación Visor de Configuraciones

Aquí mostramos un extracto del código 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 aplicación, debemos crear un objeto QSettings pasándole el

nombre de la organización y el de la aplicación como parámetros, para esto utilizamos las variables

organizacion y aplicacion. Luego creamos un nuevo QTreeWidget y llamamos a la función

leerConfiguraciones().

void VisorConfiguraciones::leerConfiguraciones()

{

QSettings config(organizacion, aplicacion);

82 10. Clases para Visualizar Elementos (Clases Item View)

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 configuración de una aplicación son guardados en una jerarquía de claves y valores. La

función 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 función recorre una estructura de árbol arbitraria llamándose así misma

recursivamente. La llamada inicial desde leerConfiguraciones() pasa un cero como padre para

representar el elemento raíz.

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 función 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 (transformándolo en un elemento de

más 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 continuación, recorremos los grupos de cada nivel. Por cada grupo, creamos un nuevo

QTreeWidgetItem, colocando el nombre del grupo en la primera columna. La función se vuelve a llamar

así misma para cargar los elementos que contiene el grupo.

Los widgets mostrados en esta sección permiten usar un estilo de programación 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 edición) escribirlo al origen de datos. En la siguiente

sección iremos más allá de este simple enfoque y mostraremos la ventaja real y completa de la arquitectura

modelo/vista de Qt.

83 10. Clases para Visualizar Elementos (Clases Item View)

Usando Modelos Predefinidos

Qt provee varios modelos predefinidos para utilizar con las vistas:

En esta sección, veremos como usar las clases QStringListModel, QDirModel y

QSortFilterProxyModel. Los modelos para el manejo de datos SQL se cubrirán en el Capítulo 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 líder de un equipo.

Figura 10.6. La aplicación Líderes de Equipos

A continuación 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 continuación creamos un QListView y

establecemos su modelo. También activamos algunos disparadores de edición que le permitirán al usuario

QStringListModel Almacena una lista de cadenas de caracteres

QStandardItemModel Almacena datos jerárquicos de cualquier tipo

QDirModel Encapsula el acceso al sistema de archivos locales

QSqlQueryModel Encapsula el acceso a bases de datos SQL

QSqlTableModel Encapsula una tabla SQL

QSqlRelationalTableModel Encapsula el acceso a tablas SQL con claves foráneas

QSortFilterProxyModel Ordena y/o filtra los datos contenidos en otros modelos

84 10. Clases para Visualizar Elementos (Clases Item View)

modificar un valor de la lista simplemente comenzando a escribir o por medio de un doble click. Por defecto,

no hay ningún disparador de edición 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 botón Insertar, se invoca al slot insertar(). Este obtiene el número 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

próxima sección, por ahora bastará con saber que un índice tiene tres componentes principales: un número de

fila, un número de columna y un puntero al modelo que pertenece. Para un modelo unidimensional la

columna siempre es 0.

Una vez que tenemos el número de fila, insertamos una nueva en dicha posición. La inserción es realizada

sobre el modelo, y el modelo automáticamente 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 edición 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 señal clicked() del botón 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 inserción, el modelo se encargará de actualizar la vista.

QStringList DialogoLiderEquipo::lideres() const

{

return modelo->stringList();

}

Por ultimo, la función lideres() proporciona una manera de obtener las lista de cadenas editadas cuando

cerremos el dialogo.

Este ejemplo podría fácilmente convertirse en un editor genérico de listas de cadenas de caracteres con solo

parametrizar el titulo de la ventana. Otro dialogo genérico que es muy requerido es aquel que presenta una

lista de archivos y directorios al usuario. En el próximo 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.

Empezáremos con la creación y configuración 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);

85 10. Clases para Visualizar Elementos (Clases Item View)

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 aplicación Visor de Directorios

Una vez que hemos construido el modelo, lo hacemos editable y establecemos algunos atributos de

ordenación. 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 ordenación

controlada por el usuario. La llamada a setClickable(true) hace que los encabezados de columna

respondan a los clicks del ratón emitiendo la señal 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 función expand()) y nos desplazamos

hasta el usando scrollTo(). Después, hacemos que la primera columna sea lo bastante ancha como para

mostrar los datos sin recortarlos.

En la parte del código 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 botón 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())

86 10. Clases para Visualizar Elementos (Clases Item View)

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 función 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

operación falla, esta devuelve un índice inválido.

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 botón 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 práctica función

que trabaja con los índices del modelo.

El último ejemplo de esta sección muestra cómo 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 expresión de filtro en un QLineEdit y especificar como dicha cadena tiene que

ser interpretada (como una expresión regular, comodines o una cadena fija) por medio de un combobox.

Figura 10.8. La aplicación Nombres de Colores

Aquí presentamos una parte del código del constructor de la clase DialogoNombreClores:

87 10. Clases para Visualizar Elementos (Clases Item View)

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 construcción

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 función

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 patrón de sintaxis al valor seleccionado en el combo. Cuando

llamamos a setFilterRegExp(), el nuevo filtro se transforma en activo y la vista es actualizada

automáticamente.

Implementando Modelos Personalizados

Los modelos predefinidos ofrecen una manera cómoda de manipular y mostrar datos. Como es normal,

algunos orígenes 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 creación 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. Decíamos anteriormente en este

capítulo que los roles usados más 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 presentación de los datos (tales como

Qt::FontRole, Qt::TextAlignmentRole, Qt::TextColorRole y

Qt::BackgroundColorRole).

88 10. Clases para Visualizar Elementos (Clases Item View)

Figura 10.9. Vista esquemática de los modelos de Qt

Para un modelo de tipo lista, el único componente relevante del índice es el número de fila, asequible por

medio de QModelIndex::row(). Para un modelo de tipo tabla, en cambio los componentes relevantes

del índice son el número 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 raíz, el cual es representado por medio de un índice inválido. Los dos primeros ejemplos de esta

sección muestran cómo 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 más alto nivel es el elemento raíz, pero cada

padre de los restantes elementos es un elemento válido en la jerarquía.

El padre de cada elemento es asequible por medio de QModelIndex::parent().

Cada elemento tiene aparte de sus datos, cero o más 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 sección.

Nuestro primer ejemplo es un modelo de solo lectura que muestra una tabla con las monedas del

mundo y la relación de cambio que existe entre las mismas.

Figura 10.10. La aplicación Monedas Circulantes

La aplicación podría implementarse fácilmente usando una simple tabla, pero queremos usar un modelo

propio aprovechando ciertas propiedades para minimizar los datos almacenados. Si almacenáramos los 162

valores monetarios actualmente negociados en una tabla, necesitaríamos 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 relación con el dolar norteamericano).

89 10. Clases para Visualizar Elementos (Clases Item View)

La clase ModeloMonetario será usada con un QTableView común. El modelo almacena los valores en

un QMap<QString,double>; cada clave es un código monetario y cada valor corresponde al tipo de

cambio de la moneda con respecto al dolar norteamericano. El siguiente fragmento de código muestra cómo

se cargan los datos en el QMap y cómo 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 implementación 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 básicos, 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 más 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, también reimplementamos headerData(), y agregamos una función para

inicializar los datos (setMonedas()).

ModeloMonetario::ModeloMonetario(QObject *parent)

: QAbstractTableModel(parent)

{

}

90 10. Clases para Visualizar Elementos (Clases Item View)

No necesitamos hacer nada en el constructor, excepto pasar el parámetro padre a la clase base

int ModeloMonetario::rowCount(const QModelIndex &

/* parent */) const

{

return mapMonedas.count();

}

Para este modelo, la cantidad de filas (y también de columnas) está dada por la cantidad de entradas en el

mapa de monedas. El parámetro 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

jerárquicas.

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 función 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

número de fila y su número de columna, disponibles a través de las funciones row() y column()

respectivamente.

Si el rol es Qt::TextAlignmentRole, devolvemos una alineación adecuada para números. Si el rol es

Qt::DisplayRole, buscamos el valor de cada moneda y calculamos el tipo de cambio. Podríamos

devolver el valor calculado como un double, pero no tendríamos control sobre la cantidad de dígitos

decimales que se mostrarían (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 función headerData() es llamada por la vista para establecer el título de los encabezados horizontales

y verticales. El parámetro section indica el número de fila o columna (dependiendo de la orientación). Ya

que las filas y columnas tienen el mismo código monetario, no tenemos que preocuparnos por la orientación

y simplemente retornamos el código de acuerdo al número de sección.

91 10. Clases para Visualizar Elementos (Clases Item View)

void ModeloMonetario::setMonedas(const QMap<QString,

double> &valores)

{

mapMonedas = valores;

reset();

}

Se puede cargar o cambiar los valores monetarios por medio de la función setMonedas(). La llamada a

QAbstractItemModel::reset() le indica a las vistas que están usando un modelo cuyos datos son

inválidos; esto fuerza a refrescar todos los elementos visibles.

QString ModeloMonetario::monedaAt(int desp) const

{

return (mapMonedas.begin() + desp).key();

}

La función monedaAt() devuelve la clave monetaria que se encuentra en la posición marcada por el

parámetro desp. Usamos un iterador de tipo STL para encontrar el elemento y luego llamamos a key().

Como acabamos de ver, no es difícil 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

diseñado. En el próximo ejemplo, la aplicación Ciudades es también un modelo de tipo tabla, pero esta vez

los datos van a ser ingresados por el usuario.

Esta aplicación es usada para almacenar valores que indican la distancia entre dos ciudades. Como en el

ejemplo anterior, podríamos simplemente usar un QTableWidget y almacenar un elemento por cada par

de ciudades. Pero un modelo propio podría ser más eficiente porque la distancia de una ciudad A a cualquier

ciudad B es la misma si vamos de A a B o al revés, por lo tanto los elementos están espejados a lo largo de la

diagonal principal.

Para ver la comparación entre el modelo propio con una simple tabla, asumiremos que tenemos tres

ciudades: A, B y C. Si almacenáramos los valores para cada combinación necesitaríamos nueve valores. Un

modelo cuidadosamente diseñado solo requiere tres elementos (A, B), (A, C) y (B, C).

Figura 10.12. La aplicación Ciudades

Aquí mostramos cómo 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);

92 10. Clases para Visualizar Elementos (Clases Item View)

QTableView ViewTabla;

ViewTabla.setModel(&modelo);

ViewTabla.setAlternatingRowColors(true);

Debemos reimplementar las mismas funciones que en el ejemplo anterior. Sumado a esto, debemos también

reimplementar las funciones setData() y flags() para que el modelo permita la edición de los datos.

Esta es la definición 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 más 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) {

93 10. Clases para Visualizar Elementos (Clases Item View)

if (index.row() == index.column())

return 0;

int pos = posicionDe(index.row(), index.column());

return distancias[pos];

}

return QVariant();

}

La función data() es similar a la construida en la clase ModeloMonetario. Devuelve 0 si la fila y

columna son iguales, porque esto correspondería 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 función 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 posición 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 función 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 función almacena el valor que

ingresó el usuario en el vector de distancias.

La función 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 función toma como argumento la fila

antes que la columna, pero invertimos los parámetros para obtener el índice del elemento diagonalmente

opuesto al que es especificado por index.

Emitimos la señal dataChanged() con el índice del elemento que fue modificado. La razón de que esta

señal tome dos índices de modelos como argumentos es que es posible un cambio que afecte a una región

rectangular de más de una fila y una columna, por lo tanto los índices indican el elemento superior izquierdo

y el inferior derecho de la región afectada. También emitimos la señal dataChanged() para el índice

transpuesto para asegurarnos de que la vista refrescará dicho elemento. Finalmente devolvemos verdadero o

falso para indicar si la edición se completó o no satisfactoriamente.

94 10. Clases para Visualizar Elementos (Clases Item View)

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 función flags() es usada por el modelo para comunicar lo que se puede hacer con el elemento (por

ejemplo, si este es editable). La implementación 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 están 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 función 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 función

devolvería 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 cálculo y por lo tanto obtenemos el mismo resultado.

El último ejemplo de esta sección es un modelo que muestra el árbol de sintaxis de una expresión regular.

Una expresión regular consiste en uno o mas términos, separados por el carácter „|‟. De este modo la cadena

“alpha|bravo|charlie” contiene tres términos. Cada término es una secuencia de uno o más factores; por

ejemplo, el término "bravo" consiste en cinco factores (cada letra es un factor). Los factores pueden ser

descompuestos en una unidad atómica más un calificador opcional, tal como '*' o '+'. Ya que las expresiones

regulares pueden tener subexpesiones entre paréntesis, pueden generar arboles de análisis recursivos.

La expresión regular mostrada en la Figura 10.14, “ab|(cd)?e”, busca un carácter '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 expresión

encontrará términos como “ab” y “cde”, pero no “bc” o “cd”.

Figura 10.13. Las estructuras de datos ciudades y distancias junto con el table model

95 10. Clases para Visualizar Elementos (Clases Item View)

La aplicación Analizador Expreg consta de cuatro clases:

VentanaExpReg: es una ventana que le permite al usuario ingresar una expresión regular y

muestra el correspondiente árbol de análisis.

AnalizadorExpReg: genera el árbol de análisis a partir de una expresión regular.

ModeloExpReg: es un modelo jerárquico que encapsula el árbol de análisis.

Nodo: representa un elemento del árbol de análisis.

Figura 10.14. La aplicación 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 vacía), un padre (el cual puede ser

cero), y una lista de nodos hijos (la cual puede estar vacía).

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 públicos, el

código que haga uso de la clase Nodo puede manipular el tipo, la cadena, el padre y los hijos directamente.

Nodo::~Nodo()

{

qDeleteAll(hijos);

96 10. Clases para Visualizar Elementos (Clases Item View)

}

La función 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 común 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 jerárquico. Las funciones esenciales que debemos reimplementar siguen siendo

las mismas, además de index() y parent(). Para establecer los datos del modelo tenemos a la función

setNodoRaiz(), a la cual se le debe pasar el nodo raíz del árbol de análisis de la expresión regular.

ModeloExpReg::ModeloExpReg(QObject *parent)

: QAbstractItemModel(parent)

{

nodoRaiz = 0;

}

En el constructor del modelo, solo necesitamos establecer el nodo raíz a un puntero nulo y pasar el padre a la

clase base.

ModeloExpReg::~ModeloExpReg()

{

delete nodoRaiz;

}

En el destructor borramos el nodo raíz, Si el nodo raíz tiene hijos, cada uno de ellos será eliminado y sus

hijos también, por el destructor de la clase Nodo.

void ModeloExpReg::setNodoRaiz(Nodo *nodo)

{

delete nodoRaiz;

nodoRaiz = nodo;

reset();

}

Cuando recibimos un nuevo nodo raíz, comenzamos por borrar el anterior. Luego establecemos el nuevo

nodo raíz y llamamos a reset() para notificar a las vistas que deben refrescar los elementos visibles.

97 10. Clases para Visualizar Elementos (Clases Item View)

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 función 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 inválido). Para un modelo de tipo tabla o lista,

no necesitamos reimplementar esta función, porque la implementación por defecto de

QAbstractListModel y QAbstractTableModel es suficiente. En nuestra función index(), si el

modelo no tiene datos devolvemos un QModelIndex inválido. De cualquier otra manera, creamos un

QModelIndex con la fila y la columna indicadas y un puntero al nodo requerido. Para modelos

jerárquicos, conocer la fila y la columna de un elemento relativo a su padre no es suficiente para poder

identificarlo: debemos conocer quién es el padre. Para resolver esto, podemos almacenar un puntero al nodo

en el índice de modelo. El objeto QModelIndex nos da la opción de guardar un void * o un entero

además del número de fila y de columna.

El nodo padre es extraído del índice usando la función privada nodoDesdeIndice(). El puntero al nodo

es obtenido a través 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 función nodoDesdeIndice() convierte el índice pasado a un puntero tipo Nodo, o devuelve el nodo

raíz si el índice es inválido, ya que un índice no válido es usado para representar el elemento raíz 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);

98 10. Clases para Visualizar Elementos (Clases Item View)

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 más complicado que buscar un hijo desde el

padre. Podemos fácilmente recuperar el nodo padre usando nodoDesdeIndice() y subiendo en la

jerarquía por medio del puntero que cada nodo tiene a su padre, pero para obtener el número de fila,

necesitamos llegar hasta el nodo abuelo y buscar el índice que indica la posición 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("Expresión Regular");

case Nodo::Expression:

return tr("Expresión");

case Nodo::Term:

return tr("Termino");

case Nodo::Factor:

return tr("Factor");

case Nodo::Atom:

return tr("Unidad Atómica");

case Nodo::Terminal:

return tr("Terminal");

default:

return tr("Desconocido");

}

} else if (index.column() == 1) {

return nodo->str;

}

return QVariant();

}

En la función 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 válido. 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 ==

99 10. Clases para Visualizar Elementos (Clases Item View)

Qt::DisplayRole) {

if (section == 0) {

return tr("Nodo");

} else if (section == 1) {

return tr("Valor");

}

}

return QVariant();

}

En la reimplementación de headerData(), devolvemos la etiqueta del encabezado horizontal o vertical

apropiada. La clase QTreeView, la cual es usada para la visualización de modelos jerárquicos no tiene

encabezados verticales, por lo tanto ignoramos esa posibilidad.

Ahora que tenemos cubierto las clases Nodo y ModeloExpReg, veremos cómo crear el nodo raíz 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 aplicación, 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 raíz del árbol analizador.

No hemos mostrado la clase AnalizadorExpReg porque no es relevante para la interfaz o para la

programación modelo/vista.

En esta sección, hemos visto cómo crear tres modelos diferentes. Muchos modelos son mucho más simples

que los que hemos mostrado aquí, con una correspondencia de uno-a-uno entre los ítems y los índices de los

modelos. Más ejemplos modelo/vista son provistos junto con Qt, e incluyen una documentación extensa para

entenderlos lo mejor posible.

Implementando Delegados Personalizados

Cada ítem de las vistas es dibujado y editado usando delegados. En la mayoría de los casos, usar el delegado

predeterminado de una vista es suficiente. Si lo que deseamos es tener un control más 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 números 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 continuación hace uso de un

delegado personalizado. Muestra los títulos de las músicas y su duración. Los datos contenidos por el modelo

serán simplemente datos tipo QString (los titulos) e int (segundos), pero la duración 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;

100 10. Clases para Visualizar Elementos (Clases Item View)

int duracion;

};

Figura 10.15. El dialogo Editor de Pistas

Aquí hay un extracto del constructor que muestra la creación 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, pasándolo en la columna que contiene el dato de los tiempos de duración.

Comenzamos estableciendo los encabezados de las columnas, luego iteramos sobre los datos, llenando las

filas con el nombre y la duración 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 edición 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;

101 10. Clases para Visualizar Elementos (Clases Item View)

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 implementación que

tiene el delegado predeterminado. Pudimos haber usado también QAbstractItemDelegate como clase

base, si queríamos empezar desde cero. Para darle la capacidad a un delegado de que pueda editar datos,

debemos implementar las funciones createEditor(), setEditorData() y setModelData().

También debemos implementar una función de dibujo, que por defecto es llamada paint(), para cambiar

el dibujado de la columna duración.

PistaDelegate::PistaDelegate(int columnaDuracion,

QObject *parent): QItemDelegate(parent)

{

this->columnaDuracion = columnaDuracion;

}

El parámetro columnaDuracion pasado al constructor le dice al delegado cuál columna contiene la

duración 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 duración de cada pista en la forma “minutos:segundos”, hemos reimplementado

la función paint(). Los llamados a arg() toman como parámetros un entero para dibujar como una

cadena, la cantidad de caracteres que debe tener, la base del entero (10 por decimal) y un carácter 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 rectángulo de enfoque o

selección 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.

102 10. Clases para Visualizar Elementos (Clases Item View)

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 edición del campo duración de cada pista, dejándole la edición de los

nombre al delegado predeterminado. Podemos lograr eso verificando cuál columna es requerida por el

delegado para proporcionar un editor para esta. Si es la columna duración la que se requiere, creamos un

QTimeEdit, establecemos el formato de visualización apropiadamente, y conectamos la señal

editingFinished() del QTimeEdit a nuestro slot commitYCerrarEditor(). Para cualquier otra

columna, pasamos el manejo de edición 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 selección fuera del QTimeEdit (pero no si presiona

Escape), la señal editingFinished() es emitida y el slot commitYCerrarEditor() es llamado.

Este slot emite la señal commitData() para informarle a la vista que existen datos editados y que debe

reemplazar los datos que se están mostrando en ella. También emite la señal 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 señal

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 función 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 duración, extraemos la duración de la pista en segundos y establecemos el tiempo del

QTimeEdit al número 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) {

103 10. Clases para Visualizar Elementos (Clases Item View)

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 edición (por ejemplo, presionando el click izquierdo afuera del widget editor o

presionando Enter o Tabulación) y no la cancela, el modelo debe ser actualizado con los datos del editor. Si

la duración fue editada, entonces extraemos los minutos y los segundos del QTimeEdit y establecemos los

datos al número de segundos correspondientes.

Aunque no es necesario en este caso, es totalmente posible crear un delegado personalizado que controle

finamente la edición y el dibujado de cualquier ítem en un modelo. Hemos elegido tomar el control de una

columna en específico, pero como QModelIndex es pasado a todas las funciones de QItemDelegate

que hemos reimplementado, podemos tomar el control por columna, fila, región rectangular, padre, o icluso

cualquier combinación 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 cómo usar las subclases convenientes de la vista, cómo usar los modelos predefinidos de Qt

y cómo 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, podríamos 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 gráfico tipo torta.

Es totalmente posible usar varias vistas para visualizar el mismo modelo sin ninguna formalidad. Cualquier

edición realizada a través de una de las vistas será automática e inmediatamente reflejada en las demás. Este

tipo de funcionalidad es particularmente útil para la visualización de conjuntos de datos muy extensos donde

el usuario desea solamente visualizar una parte de estos. La arquitectura también soporta selecciones: donde

dos o más vistas están 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 documentación online de Qt proporciona amplia cobertura al tema de la programación 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-view-programming.html para obtener información adicional y enlaces a ejemplos relevantes incluidos en Qt.

104 11. Clases Contenedoras

11. Clases Contenedoras

Contenedores Secuenciales

Contenedores Asociativos

Algoritmos Genéricos

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

Las clases contenedoras son clases tipo plantillas de propósitos generales que guardan en memoria elementos

de un determinado tipo. El lenguaje C++ ya ofrece muchos contenedores como parte de la Librería de

Plantilla Estándar, conocida en inglés como Standard Template Library (STL), la cual está incluida en las

librerías estándares 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 además todas

son implícitamente compartidas. El compartimiento implícito, o “copy on write”, es una optimización que

hace posible pasar contendores enteros como valores sin ningún coste de rendimiento significante. Los

contenedores de Qt también presentan clases iteradoras fáciles de usar inspiradas por el lenguaje Java, estas

pueden ser puestas en funcionamiento usando la clase QDataStream, y generalmente resultan en menos

códigos en el ejecutable que en los contenedores STL correspondientes. Finalmente, en algunas plataformas

hardware soportadas por Qtopia Core (la versión de Qt para dispositivos móviles), los contenedores de Qt

son los únicos que se encuentran disponibles.

Qt ofrece contenedores secuenciales como QVector<T>, QLinkedList<T> y QList<T>. También

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 también proporciona algoritmos genéricos que realizan ciertas operaciones en los contenedores. Por

ejemplo, El algoritmo qSort() ordena un contenedor secuencial, y qBinaryFind() realiza una

búsqueda binaria en un contenedor ordenado secuencialmente. Estos algoritmos son similares a aquellos

ofrecidos por STL.

Si ya estás 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 adición a las de Qt. Para más

información 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 capítulo, también veremos las clases QString, QByteArray y QVariant, ya que estas tienen

mucho en común 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 mayoría de los tipos de datos Qt y C++.

105 11. Clases Contenedoras

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++ común es que un vector posee su

propio tamaño y puede ser redimensionado. Anexar elementos extras al final de un vector resulta ser muy

eficiente, mientras que la inserción de elementos al principio o en el medio de un vector puede ser algo

costoso.

Figura 11.1. Un vector de doubles

0 1 2 3 4

937.81 25.984 308.74 310.92 40.9

Si sabemos de antemano cuántos elementos vamos a necesitar, podemos darle al vector un tamaño inicial

cuando lo definamos y usamos el operador [ ] para asignar un valor a los elementos; de otra forma,

debemos redimensionar también el vector o anexar elementos. Aquí está un ejemplo donde se especifica el

tamaño 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 vacío y usando la función append()

para anexar elementos al final:

QVector<double> vect;

vect.append(1.0);

vect.append(0.540302);

vect.append(-0.416147);

También podemos usar el operador << en lugar de usar el método append():

vect << 1.0 << 0.540302 << -0.416747;

Una manera de iterar sobre los elementos del vector es usar los operadores [ ] y el método 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 básicos y los tipos de punteros son inicializados en cero.

La inserción de elementos al principio o en el medio de un QVector<T>, o la remoción de estos de esas

posiciones, puede ser ineficiente para vectores grandes. Por esta razón, Qt también ofrece la clase

QLinkedList<T>, una estructura de datos que guarda sus ítems en una ubicación no adyacente de la

memoria. A distinción 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

106 11. Clases Contenedoras

Las listas enlazadas no proporcionan el operador [ ], de manera que los iteradores deben ser usados para

recorrer sus elementos. Los iteradores también son usados para especificar la posición de elementos. Por

ejemplo, el siguiente código 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 revisión más detallada a los iteradores más adelante en esta sección.

El contenedor secuencial QList<T> es una “lista de arreglos” que combina en una sola clase los beneficios

más 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 inserción y remoción de un elemento en ambos

extremos de un QList<T> son muy rápidas, y la inserción en el medio es también rápida para aquellas

listas no más 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 más 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 más versátil para el manejo de cadenas de texto. Lo que tiene que ver con la clase

QStringList es discutido en la última sección de este capítulo.

Las clases QStack<T> y QQueue<T> poseen dos ejemplos más de subclases convenientes. QStack<T>

es un vector que proporciona los métodos push(), pop() y top(). QQueue<T> es una lista que

proporciona los métodos 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 ningún argumento), un

constructor copia y un operador de asignación.

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 asignación. En la práctica, esto no es ningún 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 también puede ser un contenedor, caso en el cual debemos recordar separar el símbolo

“mayor que” (>) con espacios; ya que de no hacerlo, el compilador podría generar un error al tomarlos como

un operador >>. Por ejemplo:

QList<QVector<double> > list; //manera correcta

QList<QVector<double>> list; //manera incorrecta

Además 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; }

107 11. Clases Contenedoras

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

máximo dos argumentos); ya que solo posee argumentos opcionales. Esta también posee un constructor

copia y un operador de asignación, ambos provistos por C++ implícitamente. Para esta clase, copiar miembro

por miembro es suficiente, así que no necesitamos implementar nuestro propio constructor copia ni nuestro

operador de asignación.

Qt proporciona dos categorías 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 fáciles de usar,

mientras que los de estilo STL pueden combinarse, para ser más poderosos, con los algoritmos genéricos 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, después del

último elemento o entre dos elementos. Un ciclo de iteración típico, luciría algo como esto:

QList<double> lista;

...

QListIterator<double> i(lista);

while (i.hasNext()) {

Sequential Containers 255

do_something(i.next());

}

Figura 11.3. Posiciones válidas 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

función next() retorna el elemento a la derecha del iterador y avanza el iterador de la siguiente posición

valida.

Iterar en reversa es algo similar, excepto que primero debemos llamar a la función toBack() para

posicionar el iterador después del último elemento:

QListIterator<double> i(lista);

i.toBack();

while (i.hasPrevious()) {

do_something(i.previous());

}

La función 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 posición hacia atrás. Otra manera de pensar

acerca de los iteradores next() y previous() es que estos pueden retornar el elemento que el iterador

tiene justo después de él, bien sea para adelante o para atrás.

108 11. Clases Contenedoras

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 números negativos de una lista:

QMutableListIterator<double> i(lista);

while (i.hasNext()) {

if (i.next() < 0.0)

i.remove();

}

La función remove() siempre opera en el último elemento al que se ha saltado. También 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 función llamada setValue() que

modifica el último elemento al que se ha saltado. Aquí se muestra la manera de cómo reemplazar números

negativos con su valor absoluto:

QMutableListIterator<double> i(lista);

while (i.hasNext()) {

int val = i.next();

if (val < 0.0)

i.setValue(-val);

}

También es posible insertar un elemento en la posición actual de un iterador llamando al método

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 función 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 posición

siguiente del último elemento (por ejemplo, lista[5] para una lista de tamaño 5). Si el contenedor está

vacío, la función begin() y la función end() retornan lo mismo. Esto puede ser usado para ver si el

contenedor no posee ningún ítem, aunque usualmente es más conveniente llamar a isEmpty() para ese

propósito.

Figura 11.5. Posiciones válidas 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

109 11. Clases Contenedoras

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 función

usando un iterador estilo STL, debemos hacer una copia del contenedor e iterar sobre ella. Por ejemplo, el

siguiente código 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 código 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á automáticamente antes de que

hayamos empezado a iterar, dejándonos con un iterador inservible. Para complicarlo más 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 función 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 optimización

llamada compartición implícita (implicit sharing). Esto quiere decir que copiar un contenedor Qt es casi tan

rápido como copiar un puntero. Solo si una de las copias ha cambiado sus datos actualmente copiados –y

todo esto es manejado automáticamente. Por esta razón, la compartición implícita es llamada algunas veces

“copiar sobre escritura” (“copy on write”).

La belleza de la compartición implícita radica en que esta es una optimización en la cual no necesitamos

pensar; simplemente funciona, sin requerir ninguna intervención por parte del programador. Al mismo

tiempo, la compartición implícita promueve un estilo de programación limpio donde los objetos son

retornados por valor. Considere la siguiente función:

QVector<double> tablaDelSeno()

{

QVector<double> vect(360);

110 11. Clases Contenedoras

for (int i = 0; i < 360; ++i)

vect[i] = sin(i / (2 * M_PI));

return vect;

}

El llamado a esta función se vería así:

QVector<double> tabla = tablaDelSeno();

STL, en comparación, 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 función 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 función se hace más tedioso para escribirlo y menos claro de leer:

vector<double> tabla;

tablaDelSeno(tabla);

Qt usa la compartición implícita 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 parámetros de funciones como valores retornados.

La compartición implícita es una garantía de que los datos no serán copiados si no los modificamos. Para

obtener lo mejor de la compartición implícita, podemos adoptar, como programadores, un par de nuevos

hábitos de programación. Un hábito es usar la función 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 asignación 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 asignación.

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 solución es usar

const_iterator, constBegin() y constEnd() cuando sea posible.

Qt proporciona un último método 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 términos del for estándar. En cada iteración del

ciclo, la variable de iteración (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 razón el ciclo no se ve afectado si el contenedor es modificado durante la iteración.

111 11. Clases Contenedoras

Cómo Funciona La Compartición Implícita (Implicit Sharing)

La compartición implícita trabaja automáticamente tras bastidores, de manera que no tenemos que hacer

nada en nuestro código para hacer que esta optimización ocurra. Pero, como siempre es bueno saber cómo

funcionan las cosas, estudiaremos un ejemplo y veremos qué sucede debajo del capó. El ejemplo usa la

clase QString, una de las muchas clases implícitas 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 función 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 ningún 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 múltiples

hilos, a raíz 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 atómicas. Esta tecnología se encuentra disponible para los usuarios de Qt a

través de las clases QSharedData y QSharedDataPointer.

Las palabras reservadas para los ciclos break y continue están soportadas. Si el cuerpo consta de una

sola sentencia, las llaves son innecesarias. Como sucede con una sentencia for, la variable de iteración

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 iteración fuera del ciclo es la única opción para aquellos contenedores que contienen

una coma (por ejemplo, QPair<QString, int>).

112 11. Clases Contenedoras

Contenedores Asociativos

Un contenedor asociativo contiene un número 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 distribución hace posible obtener un buen performance

en las tareas búsquedas e inserción y también en la iteración in-orden. Internamente, QMap<K, T> está

implementada como una lista de saltos (skip-list).

Figura 11.6. Mapa de QString a int

Una manera sencilla de insertar elementos en un mapa es llamando al método 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 creación accidental de valores, podemos usar la función

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

ningún elemento nuevo será creado. Para los tipos básicos y punteros, retorna cero. Podemos especificar

también cualquier otro valor por defecto como un segundo argumento a la función 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 básicos como int o double, tipos

punteros, o clases que tengan un constructor por defecto, un constructor copia y un operador de asignación.

Además, 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).

Mexico City

Seoul

Tokyo

22 350 000

22 050 000

34 000 000

113 11. Clases Contenedoras

QMap<K, T> posee una pareja de funciones convenientes: keys() y values(), que son especialmente

útiles cuando tratamos con pequeños 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 múltiples pares de clave-valor con la misma clave usando la función insertMulti() o la subclase

conveniente QMultiMap<K, T>. La clase QMap<K, T> tiene una función 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 idéntica a la de QMap<K, T>, pero posee requerimientos diferentes para el tipo de dato K y

usualmente proporciona operaciones de búsqueda mucho más rápidas que las que puede lograr QMap<K,

T>. Otra diferencia es que, en QHash<K, T>, los elementos están desordenados.

En adición a los requerimientos estándares 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 función

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 automáticamente un número primo de revisión para sus tablas hash internas y las

redimensiona tantas veces como ítems son insertados o removidos de estas. También es posible hacer

pequeños ajustes de rendimiento llamando a la función reserve() para especificar el número esperado de

ítems que se esperan alojar en el hash. También se usa squeeze() para encoger la tabla hash basándose en

el número actual de ítems. Un habito suele ser llamar a reserve() con el número máximo de ítems que

esperamos guardar, luego insertar los datos, para finalmente, llamar a squeeze() para minimizar el uso de

memoria si habían 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 función insertMulti() o la subclase conveniente

QMultiHash<K, T>.

A parte de QHash<K, T>, Qt también proporciona la clase QCache<K, T> que puede ser usada para

alojar en caché los objetos, asociándolos 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 más fácil de iterar a través 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 través 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();

114 11. Clases Contenedoras

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 función 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 también proporcionan las funciones key() y value(). Con los tipos de

iteradores no constantes, la función value() retorna una referencia no constante, permitiéndonos cambiar

el valor así como lo iteramos. Nota, que aunque estos iteradores son llamados de “estilo STL”, estos se

desvían significantemente de los iteradores map<K, T> de STL, los cuales están basados en la plantilla

pair<K, T>.

El ciclo foreach también 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 también 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 Genéricos

La cabecera <QtAlgorithms> declara una serie de funciones de plantillas globales que implementan

algoritmos básicos en los contenedores. La mayoría de estas funciones operan en iteradores estilo STL.

La cabecera STL <algorithm> proporciona un conjunto más completo de algoritmos genéricos. Estos

algoritmos pueden ser usados en los contenedores de Qt, así como también en los contenedores STL. Si las

implementaciones STL están 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 operación 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");

115 11. Clases Contenedoras

QStringList::iterator j = qFind(lista.begin(), lista.end(), "Petra");

El algoritmo qBinaryFind() realiza una búsqueda de la misma manera que lo hace qFind(), excepto

que qBinaryFind() asume que los elementos a buscar ya están ordenados de manera ascendiente y usa

su rápido método de búsqueda binaria en lugar de la búsqueda 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 porción del

contenedor variando los argumentos. El siguiente segmento de código 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() también 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 código, 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 ordenación. Por ejemplo, aquí se muestra una

comparación “menor que” que compara dos QStrings de manera no sensible a las mayúsculas:

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 tenían antes de la ordenación. 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 método anteriormente en el Capítulo 4 para

implementar el ordenamiento en la aplicación Hoja de Cálculo.

116 11. Clases Contenedoras

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. Después

de la llamada a esta función, aun debe hacerse el llamado a la función 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 función qAbs(), que retorna el valor absoluto de sus argumentos, y las

funciones qMin() y qMax(), que retornan el mínimo y el máximo 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 común 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 compartición implícita como una optimización 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 también), no solamente para la interfaz de usuario sino también

como estructuras de datos. El lenguaje C++ proporciona nativamente dos tipos de cadenas: las cadenas

tradicionales de C, que poseen el carácter de finalización de cadenas „\0‟ y la clase std::string. A

distinción de estas, QString contiene valores Unicode de 16-bits. Unicode contiene ASCII y Latin-1 como

un conjunto, con sus valores numéricos usuales. Pero ya que QString es de 16-bits, este puede representar

miles de otros caracteres para escribir la mayoría de los lenguajes del mundo. Vea el Capitulo 17 para más

información 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 función

length() retorna el tamaño 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 través de la repetición de anexar caracteres es muy rápido. Aquí está un ejemplo que muestra cómo

trabajar con los operadores + y +=:

QString str = "Usuario: ";

str += nombreUsuario + "\n";

También existe una función 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 función de QString llamada

sprintf():

str.sprintf("%s %.1f%%", "competición perfecta", 100.0);

117 11. Clases Contenedoras

Esta función soporta los mismos especificadores de formato que la función sprintf() de C++. En el

ejemplo anterior, a la cadena str le es asignada la cadena “competición perfecta 100.0%”.

Todavía hay otra manera de construir una cadena a partir de otras cadenas o de números, la función 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 parámetros extras para controlar el ancho del campo, la base numérica, o la precisión de

los punto flotantes. En general, arg() es una solución mucho mejor que sprintf(), porque esta es

segura cuando el tipo de dato a incrustar es importante, soporta totalmente Unicode y permite usar intérpretes

para reordenar los parámetros “%n”.

QString puede convertir números en cadenas usando la función estática QString::number():

str = QString::number(59.6);

O usando la función setNum():

str.setNum(59.6);

La conversión inversa, de cadena de texto a número, 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 operación de conversión. Si la conversión falla, estas funciones retornan

cero.

Una vez que obtengamos una cadena, muy a menudo querremos extraer partes de esta. La función mid()

retorna la subcadena que empieza en una posición dada (el primer argumento) y extendiéndose hasta otra

posición (el segundo argumento). Por ejemplo, el siguiente código imprime “pagar” en la consola: *

QString str = "hay que pagar la renta";

qDebug() << str.mid(8, 4);

Si omitimos el segundo argumento, la función mid() retorna la subcadena que se forma al empezar en la

posición dada (8) y al terminar en el final de la cadena. Por ejemplo, el siguiente código imprime “pagar la

renta” en la consola:

QString str = "hay que pagar la renta";

qDebug() << str.mid(8);

También están 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 código 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 carácter, una subcadena o una expresión 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 inclusión del archivo de cabecera <QtDebug>,

mientras que la sintaxis (“…”,arg) está disponible en cualquier archivo que incluya al menos una cabecera de Qt.

118 11. Clases Contenedoras

El código anterior hará que la variable i valga 3. La función indexOf() retorna -1 cuando falla, y acepta

una posición 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 rápido 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 función localeAwareCompare() es usualmente la elección más indicada, y si

queremos que las comparaciones sean case-sensitive, podemos usar los métodos toUpper() y

toLower(). Por ejemplo:

if (nombreArchivo.toLower() == "leeme.txt")

...

Si queremos reemplazar cierta parte de una cadena con otra cadena, podemos usar la función replace():

QString str = "un día nublado";

str.replace(7, 7, "soleado");

El resultado es “un día soleado”. El código puede reescribirse para usar las funciones remove() e

insert():

str.remove(7, 7);

str.insert(7, "soleado");

Primero, removimos siete caracteres empezando en la posición 7, quedando la cadena “un día”, luego

insertamos “soleado” en la posición 7.

Existen versiones sobrecargadas de la función 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 línea) de una cadena. QString posee una función 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 función 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 más

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 función simplified():

119 11. Clases Contenedoras

QString str = " BOB \t EL \nPERRO \n";

qDebug() << str.simplified();

La cadena retornada por simplified() es:

B O B E L P E R R O

Una cadena puede ser dividida en un QStringList formado por subcadenas usando

QString::split():

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 función split() posee un tercer argumento opcional que

especifica si se debe quedar con subcadenas vacías (la opción por defecto) o estas deben ser descartadas.

Los elementos en un QStringList deben ser unidos para formar una sola cadena usando el método

join(). El argumento que se le envía 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 alfabéticamente y separados por saltos de línea:

palabras.sort();

str = palabras.join("\n");

Cuando se trata con cadenas de texto, regularmente solemos necesitar determinar si una cadena está vacía o

no. Esto se hace llamando al método isEmpty() o verificando si el método length() es igual a 0.

La conversión de cadenas const char * a cadenas QString es una operación automática en la mayoría

de los casos. Por ejemplo:

str += " (1870)";

Aquí agregamos un const char * a un QString sin ningún protocolo. Para convertir explícitamente un

const char * a un QString, simplemente debemos usar un cast de QString, o llamar a la función

fromASCii() o a la función fromLatin1() (Vea el Capitulo 17 para una explicación de manejo de

cadenas literales en otras codificaciones).

Para hacer una conversión 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 el macro qPrintable() que realiza lo mismo que

toAscii().constData():

printf("Usuario: %s\n", qPrintable(str));

Cuando llamamos a data() o a constData() en un QByteArray, la cadena retornada pasa a ser

poseída por el objeto QByteArray. Esto significa que no necesitamos preocuparnos de los famosos goteos

de memorias o, en inglés, 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á automáticamente eliminado al final de la declaración.

La clase QByteArray posee una API muy similar a la que tiene QString. Funciones como left(),

right(), mid(), toLower(), toUpper(), trimmed() y simplified() existen también

en QByteArray con las misma semántica 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.

120 11. Clases Contenedoras

Por comodidad, QByteArray se asegura automáticamente de que el último byte pasado el ultimo carácter

de la cadena, sea „\0‟, facilitando el pase de un QByteArray a una función que recibe un const char

*. QByteArray también soporta caracteres „\0‟ incrustados, permitiéndonos usarlo para guardar datos

binarios a placer.

En algunas situaciones, necesitamos guardar datos de tipos diferentes en la misma variable. Un método

consiste en codificar los datos como un QByteArray o un QString. Por ejemplo, una cadena puede

contener un valor textual o un valor numérico en forma de cadena. Estos métodos 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 más 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 también los tipos de datos numéricos básicos del

C++, como double e int. La clase QVariant también puede alojar contenedores:

QMap<QString,QVariant>, QStringList y QList<QVariant>.

Las variantes son usadas extensivamente por las clases de visualización de elementos (clases ítem view), los

módulos de bases de datos y QSettings, permitiéndonos 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 Capítulo 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 través 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 ["Piña"] = 3.85;

mapaFruta ["Pera"] = mapaPera;

Acá hemos creado un mapa con cadenas de texto como claves (nombres de productos) y valores que son

tanto números de punto flotante (precios) como mapas. El mapa de primer nivel contiene tres claves:

“Naranja”, “Pera” y “Piña”. 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 función 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 más 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 módulo 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 función miembro tipo

plantilla QVariant::value<T>():

QIcon icono = variant.value<QIcon>();

121 11. Clases Contenedoras

La función value<T> () también funciona para convertir entre tipos de datos no relacionados a la interfaz

de usuario (non-GUI data types) y QVariant, pero en la práctica lo que usamos normalmente son las

funciones de conversión to…() (por ejemplo, toQString()) para los tipos de datos no relacionados a la

interfaz de usuario.

QVariant puede usarse también 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

definición de la clase:

Q_DECLARE_METATYPE(TarjetaDeNegocios)

Esto nos permite escribir el código 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 están 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 capítulo se ha enfocado en los contenedores de Qt, así como también en las clases QString,

QByteArray y QVariant. Adicionalmente a estas clases, Qt también 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 sección del Capítulo 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 implícitamente, su costo operativo es menor

que la de QVector<T>, haciéndolo más 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 documentación de Qt en http://doc.trolltech.com/4.1/algorithms.html. Y

para mas detalles de los contenedores de Qt, incluyendo información acerca de su complejidad y estrategias

de desarrollo, visite http://doc.trolltech.com/4.1/containers.html.

122 12. Entrada/Salida

12. Entrada/Salida

Lectura y Escritura de Datos Binarios

Lectura y Escritura de Archivos de Texto

Navegar por Directorios

Incrustando Recursos

Comunicación entre Procesos

La lectura o escritura de archivos u otros dispositivos es una necesidad común en casi todas las aplicaciones.

Qt provee un excelente soporte para las operaciones de E/S a través de QIODevice, una poderosa

abstracción que encapsula "dispositivos" que pueden leer y escribir bloques de bytes. Qt incluye las

siguientes clases derivadas de QIODevice:

QFile Permite el acceso a archivos del sistema local y recursos embebidos.

QTemporaryFile Crea y accede archivo temporales en el sistema.

QBuffer Realiza operaciones de lectura y escritura de datos sobre QByteArray.

QProcess Ejecuta programas externos y maneja comunicación entre procesos.

QTcpSocket Transfiere un flujo de datos sobre una red usando TCP.

QUdpSocket Envía 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 leídos

cualquier cantidad de veces desde cualquier posición; estas cuentan con la función QIODevice::seek()

para posicionar el puntero del archivo.

Sumado a las clases de dispositivos, Qt también 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 ordenación de bytes y codificación de textos,

asegurando que las aplicaciones ejecutadas en diferentes plataformas o en diferentes países puedan leer y

escribir dichos archivos. Esto hace que las clase para manejo de E/S proporcionadas por Qt sean mucho más

prácticas que las correspondientes clases estándares de C++, las cuales dejan que el programador de la

aplicación 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 información sobre los

archivos que se encuentran dentro de estos.

La clase QProccess permite lanzar programas externos y comunicarse con estos a través de los canales

estándares de entrada, salida y de error (cin, cout y cerr). También podemos establecer el valor de las

variables de entorno que la aplicación externa usará y su directorio de trabajo. Por defecto, la comunicación

con el proceso es asíncrona, es decir, que no bloquea nuestra aplicación, pero es posible bloquearla en ciertas

ocasiones.

123 12. Entrada/Salida

El trabajo en red y el manejo de archivos XML son temas tan importantes que serán cubiertos separadamente

en capítulos exclusivos (Capítulo 14 y Capítulo 15).

Lectura y Escritura de Datos Binarios

La manera más simple de cargar y guardar datos binarios es instanciar la clase QFile, para abrir el archivo,

y acceder a sus datos a través de un objeto QDataStream. Esta clase provee un formato de

almacenamiento independiente de la plataforma que soporta los tipos básicos de C++ como int y double,

pero además también incluye soporte para QByteArray, QFont, QImage, QPixmap, QString y

QVariant, así como también clases contenedoras como QList<T> y QMap<K,T>.

Aquí mostramos cómo podríamos 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 situación 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 número de versión

que se utilizará. Este es un entero que indica la forma de representar los tipos de datos de Qt (los tipos

básicos de C++ son representados siempre de la misma manera). En Qt 4.1, el formato más completo es la

versión 7. Podemos establecer el valor a mano o usar un nombre simbólico (en este caso

QDataStream::Qt_4_1).

Para asegurarnos de que el número 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" (véase Endianness en el Glosario

de terminos) por defecto, esto puede modificarse por medio de setByteOrder().

No necesitamos cerrar explícitamente el archivo ya que esto se realiza automáticamente cuando la variable

QFile queda fuera de ámbito. Si queremos verificar que los datos hayan sido escritos, podemos llamar a la

función flush() y verificar su valor de retorno (true si no hubo errores).

El código 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;

124 12. Entrada/Salida

return;

}

QDataStream entrada(&archivo);

entrada.setVersion(QDataStream::Qt_4_1);

entrada >> num >> imagen >> mapa;

La versión de QDataStream que usamos para leer los datos es la misma que la usada para escribirlos.

Siempre debe ser así. Estableciendo la versión por código, nos aseguramos que la aplicación siempre pueda

leer los datos (asumiendo que ésta fue compilada con QT 4.1 o cualquier versión 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 también 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 fácil. 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 vacíos. 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 válidos.

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 definición 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;

}

125 12. Entrada/Salida

Para guardar una clase Cuadro, simplemente guardamos dos QString y un quint32. Al final de la

función, devolvemos el objeto QDataStream. Esto es algo común en C++, que nos permite encadenar

operadores << con un solo flujo de salida. Por ejemplo:

salida << cuadro1 << cuadro2 << cuadro3;.

La implementación 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 fácil:

QList<Cuadro> cuadros;

entrada >> cuadros;

Este código generaría un error de compilación 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 más 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 análisis de los datos que queremos leer. Nuestra única obligación 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 aplicación como para formatos binarios estándares.

Podemos leer y escribir formatos binarios estándares usando los operadores de flujo sobre tipos básicos

(como quint16 o float) o usando las funciones readRawBytes() y writeRawBytes(). Si

estamos usando QDataStream para leer y escribir exclusivamente tipos de datos básicos de C++, no

debemos llamar nunca a setVersion().

Hasta el momento, hemos cargado y guardado datos con la versión del flujo establecida a

QDataStream::Qt_4_1. Este enfoque es simple y seguro, pero tiene un pequeño inconveniente: no

podemos aprovechar mejoras o nuevos formatos. Por ejemplo, si en una versión posterior de Qt se agrega un

nuevo atributo a la clase QFont y nosotros hemos establecido el numero de versión a Qt_4_1, este atributo

no podría ser ni guardado ni cargado. Para este problema tenemos dos posibles soluciones. La primera sería

integrar el número de versión en el archivo:

QDataStream salida(&archivo);

salida << quint32(NumeroMagico) << quint16(salida.version());

(NumeroMagico es una constante que identifica inequívocamente el tipo de archivo). Eso nos asegura que

siempre escribamos los datos usando la versión más reciente de QDataStream, cualquiera que ésta sea.

Cuando vamos a cargar los datos desde el archivo, primero leemos la versión utilizada:

126 12. Entrada/Salida

quint32 magico;

quint16 version;

QDataStream entrada(&archivo);

entrada >> magico >> version;

if (magico != NumeroMagico) {

cerr << "El archivo no es reconocido por la aplicación" << endl;

} else if (version > entrada.version()) {

cerr << "El archivo fue creado con una versión más reciente”

“de la aplicación" << endl;

return false;

}

entrada.setVersion(version);

Podemos leer los datos del archivo siempre que la versión del flujo sea menor o igual a la versión usada por

la aplicación; si esto no se cumple, reportamos el error.

Si el formato del archivo contiene un número de versión propio, podemos usarlo para deducir la versión del

flujo usada, en vez de almacenarlo explícitamente. Por ejemplo, supongamos que la versión del formato de

archivo de nuestra aplicación 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 versión de QDataStream usada en base al número de

versión de la aplicación:

QDataStream entrada(&archivo);

entrada >> magico >> appVersion;

if (magico != NumeroMagico) {

cerr << "El archivo no es reconocido por la aplicación" << endl;

return false;

} else if (appVersion > 0x0103) {

cerr << "El archivo fue creado con una versión más reciente”

“de la aplicación"<< 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 versión anterior a 1.3 de la

aplicación usa la versión 4 (Qt_3_0), y que los archivos guardados con la versión 1.3 usan la versión 7

(Qt_4_1).

En resumen, hay tres políticas para manejar versiones de QDataStream: establecer a mano el número de

versión, escribirlo y leerlo explícitamente y usar diferentes versiones de acuerdo a la versión de la aplicación.

Cualquiera de estas pueden usarse para asegurar que los datos escritos en una versión anterior de una

aplicación puedan ser cargados en una nueva versión, aun si ésta se ha compilado con una versión posterior

de Qt. Una vez que hayamos escogido una política para manejar la versión 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:

127 12. Entrada/Salida

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 función write() para que lo guarde en otro archivo. Tener

todos los datos en un QByteArray requiere más 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 más apropiado que usar

QDataStream. La clase QIODevice provee la función peek() que devuelve el siguiente byte de datos

sin efectos secundarios mientras que la función getChar() transforma el byte en un carácter. Esto

funciona para dispositivos de acceso aleatorio (como archivos) así coma también para dispositivos

secuenciales (como sockets de red). También disponemos de la función seek() que establece la posición

del dispositivo, para aquellos que soporten acceso aleatorio.

Los archivos binarios proveen las forma más versátil y compacta de almacenar datos, y QDataStream hace

que el acceso a datos binarios sea muy fácil. Además de los ejemplos dados en esta sección, 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 típicamente más 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 código fuente. El manejo de archivos XML se cubre

debidamente en el Capitulo 15.

QTextStream se encarga de convertir entre Unicode y la codificación usada en el sistema o cualquier otra

codificación, 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. Además soporta los tipos numéricos básicos de C++, los cuales puede convertir a

cadenas y viceversa. Por ejemplo. el siguiente código 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 fácil, pero cargarlo puede ser difícil, porque los datos de tipo texto son ambiguos.

Consideremos el siguiente ejemplo:

salida << "Noruega" << "Suecia";

128 12. Entrada/Salida

Si el objeto salida es un QTextStream, el valor que se guardará será "NoruegaSuecia". No podemos

esperar que el siguiente código 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

vacío. 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, podríamos necesitar un verdadero analizador. Este se encargaría de

cargar caracter por caracter usando el operador >> con un QChar, o linea a linea usando

QTextStream::readLine(). Al final de esta sección, presentaremos dos pequeños 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á pequeño.

Por defecto, QTextStream usa la codificación de caracteres del sistema local (por ejemplo, ISO 8859-1 o

ISO 8859-15 en América y muchas partes de Europa) para leer y escribir. Esto puede modificarse por medio

de setCodec(), algo así:

stream.setCodec("UTF-8");

La codificación UTF-8 es muy popular y compatible con ASCII, puede representar el conjunto completo de

caracteres Unicode. Para más información sobre Unicode y el soporte de QTextStream para

codificaciones ver el Capitulo 17 (Internacionalización).

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 también pueden usarse a través 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 Auto-detectccion basada en prefijo (cuando lee)

2 Binary

8 Octal

10 Decimal

16 Hexadecimal

setNumberFlags(NumberFlags)

showBase Muestra un prefijo esi la base es 2 (“0b”), 8

(“0”) o 16 (“0x”)

ForceSign Siempre muestra el signo en números reales

ForcePoint Siempre coloca el separador decimal en

números

UppercaseBase Usa versiones en mayúsculas de base prefijada

(“0X”, “0B”)

129 12. Entrada/Salida

Al igual que QDataStream, QTextStream opera sobre una subclase de QIODevice, pudiendo ser

QFile, QTemporaryFile, QBuffer, QProcess, QTcpSocket, o QUdpSocket.

Además 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 codificación del flujo, ya que QString siempre trabaja

en Unicode.

Veamos un ejemplo simple de un formato de archivo basado en texto. En la aplicación Hoja de Calculo

descrita en la Parte I, usamos un formato binario para almacenar los datos. Estos consistían en una secuencia

de fila, columna, formula, uno por cada celda no vacía. Guardar estos datos como texto es sencillo; aquí

vemos un extracto de una versión modificada del método para guardar el archivo de la aplicación Hoja de

Calculo, en el método 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;

}

}

UppercaseDigits Usa letras mayúsculas en números

hexadecimales

setRealNumberNotation(RealNumberNotation)

FixedNotation Notación de Punto-fijo (p. e., “0.000123”)

ScientificNotation Notación científica (p. e., “1.234568e-04”)

SmartNotation Notación de punto-fijo o científica, la que sea

más compacta

setRealNumberPrecision(int)

Establece el número máximo de dígitos que deben ser generados (6 por defecto)

setFieldWidth(int)

Establece el tamaño mínimo de un campo (0 por defecto)

setFieldAlignment(FieldAlignment)

AlignLeft Rellena el lado derecho del campo

AlignRight Rellena el lado izquierdo del campo

AlignCenter Rellena ambos lados del campo

AlignAccountingStyle Rellena entre el signo y el número

setPadChar(QChar)

Establece el caracter usado para los campos de relleno (espacio por defecto)

130 12. Entrada/Salida

Hemos usado un formato simple, en donde cada linea representa una celda y con espacios entre los datos de

fila, columna y fórmula. La fórmula puede contener espacios en blanco, pero podemos asumir que no

contiene un carácter de fin de línea („\n‟). Ahora veamos el código 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 función 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 función

QStringList::takeFirst() quita el primer elemento de la lista y lo devuelve. Usamos esta para

extraer el número de fila y de columna. No hemos realizado ninguna comprobación de errores; si leemos un

valor no entero como número 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 técnica 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 función 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;

131 12. Entrada/Salida

}

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

función. Mantenemos tres elementos de estado: un contador de nuevas lineas, un contador de espacios y uno

que marca la posición de la columna actual en la linea, que nos sirve para convertir un tabulador en el

número de espacios correctos.

El análisis se realiza mediante un ciclo while que recorre, uno por vez, cada carácter presente en el archivo

de entrada. El código es un poco sutil en algunos lugares. Por ejemplo, aunque establezcamos el tamaño 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 indentación) 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 gráfica. 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 debería ser fácil 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 aplicación de consola, el archivo .pro presenta pequeñas diferencias con el de una aplicación

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 módulo QtGui. Después

especificamos que queremos habilitar la salida por consola en Windows y que no queremos que la aplicación

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 mayoría de las

132 12. Entrada/Salida

aplicaciones necesitan soportar otros tipos de codificación en algún que otro punto y solo QTextStream

provee apoyo para esto. Si aun quiere escribir texto directamente a un QIODevice, debe especificar

explícitamente la bandera QIODevice::Text al usar la función 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',

más allá de la convención de fin de línea usada por el sistema operativo.

Navegar por Directorios

La clase QDir provee un medio independiente de plataforma para navegar por los directorios y obtener

información sobre los archivos. Para ver cómo se usa esta clase, escribiremos una pequeña aplicación de

consola que calcula el espacio consumido por todas las imágenes que hay en un directorio particular y en

todos sus subdirectorios.

El corazón de la aplicación es la función espacioImagen(), la cual se encarga de calcular

recursivamente el tamaño ocupado por todas las imágenes 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 pasándole el directorio de trabajo, el cual puede ser relativo al directorio actual o

absoluto. Le pasamos a la función entryList() dos parámetros. 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 tamaños. La clase QFileInfo nos permite acceder a

varios atributos de los archivos, tales como el tamaño 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

tamaño de sus imágenes.

Para crear cada ruta de subdirectorios, combinamos la ruta del directorio actual con el nombre del

subdirectorio, separándolo con una barra. QDir trata al carácter el carácter '/' como el separador de

directorios en todas las plataformas, adicionalmente al reconocido carácter „\‟ que se usa en Windows.

Cuando tenemos que mostrar las rutas al usuario, podemos usar la función estática

133 12. Entrada/Salida

QDir::convertSeparators() para convertir el separador de directorios al utilizado en cada

plataforma.

Agreguemos un función main() a nuestro pequeño 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 imágenes 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, podríamos 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 función espacioImagen() para que realice el cálculo del tamaño que ocupan las imágenes.

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(). También incluye algunas funciones estáticas como remove() o exists().

Incrustando Recursos

Hasta el momento, en este capitulo, hemos hablado sobre acceder datos en dispositivos externos, pero

también es posible incluir datos binarios o de texto dentro del ejecutable de la aplicación. Esto se hace por

medio del sistema de recursos de Qt. En otros capítulos, hemos usados archivos de recursos para incluir

imágenes en el ejecutable, pero también es posible agregar cualquier tipo de archivo. Los archivos

embebidos o incrustados, pueden ser leídos usando QFile como cualquier archivo normal del sistema.

Los recursos son convertidos a código 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 aplicación que retenga información de contactos. Para comodidad

de nuestros usuarios, queremos incluir los códigos de marcado internacionales en el ejecutable. Si el archivo

se encuentran en el subdirectorio datos, dentro del directorio donde es contruida la aplicación, el archivo

de recursos podría verse algo así:

<!DOCTYPE RCC><RCC version="1.0">

<qresource>

<file>datos/cod-telefono.dat</file>

</qresource>

</RCC>

Desde la aplicación, los recursos son identificados por el prefijo ':/'. En este ejemplo, los códigos de marcado

tienen la ruta :/datos/cod-telefono.dat y puede ser leído como cualquier otro archivo usando QFile.

134 12. Entrada/Salida

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 estáticamente). Esta técnica tiene dos desventajas: si

necesitamos hacer cambios en algún dato embebido, debemos cambiar también el ejecutable, y el tamaño del

ejecutable será más grande porque debe alojar los datos incrustados.

El sistema de recursos de Qt provee más características que las presentadas en este ejemplo, incluye soporte

para alias de archivos y para localización. Todo esto está documentado en

http://doc.trolltech.com/4.1/resources.html.

Comunicación entre Procesos

La clase QProcess nos permite ejecutar programas externos e interactuar con ellos. Trabaja

asíncronamente, realizando el trabajo en segundo plano para no bloquear la interfaz de usuario. Emite

señales para notificarnois cuando el proceso externo tenga datos o haya finalizado.

Revisaremos el código de una pequeña aplicación que permite convertir un imagen a través de otro

programa. Para este ejemplo, contamos con el programa ImageMagick, el cual está disponible libremente en

todas las plataformas.

Figura 12.2. La aplicación 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);

135 12. Entrada/Salida

void procesoError(QProcess::ProcessError error);

private:

QProcess proceso;

QString archivoDestino;

};

#endif

Esta sigue el patrón de todas las clases generadas desde un formulario de Qt Designer. Gracias al mecanismo

de conexión automática, los slots on_botonBuscar_clicked() y

on_botonConvertir_clicked() ya están conectados a las señales 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 conexión entre señales

y slots necesarias. Después de esto, conectamos manualmente tres señales del objeto QProcess a tres slots

privados. Siempre que el proceso externo publique datos o errores (a través 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 señal clicked() del botón Buscar es conectada automáticamente al slot

on_botonBuscar_clicked() en la función setupUi(). Si el usuario previamente a seleccionado un

archivo, inicializamos el dialogo de selección 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";

136 12. Entrada/Salida

if (monocromoCheckBox->isChecked())

args << "-monocromo";

args << archivoOrigen << archivoDestino;

proceso.start("convertir", args);

}

Cuando el usuario presiona el botón Convertir, copiamos el nombre del archivo y cambiamos la extensión

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.

Después desactivamos el botón Convertir para prevenir el lanzamiento accidental de varias conversiones, y

limpiamos el editor de texto que usamos para mostrar información de estado.

Para iniciar el proceso externo, llamamos a QProcess::start() con el nombre del programa a ejecutar

más 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 conversión infiere el tipo de conversión requerida por la extensión 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 algún 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 conversión”

“ha finalizado inesperadamente"));

} else if (codigoSalida != 0) {

outputTextEdit->append(tr("No fue posible realizar la”

“conversión"));

} 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 botón Convertir. void DialogoConvertir::procesoError(QProcess::ProcessError

error)

{

if (error == QProcess::FailedToStart) {

outputTextEdit->append(tr("El programa de conversión “

“no fue encontrado"));

botonConvertir->setEnabled(true);

}

}

Si el proceso no se puede iniciar, QProcess emite la señal error() en vez de finished().

Reportamos cualquier error y habilitamos el botón Convertir.

137 12. Entrada/Salida

En este ejemplo, hemos realizado la conversión asíncronamente, o sea que le decimos a QProcess que

ejecute el programa de conversión y devuelva el control a la aplicación 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 aplicación, en esos casos

necesitamos que QProcess opere de manera síncrona.

Un ejemplo común en donde es deseable para la aplicación que el proceso externo se ejecute de manera

síncrona es la edición de texto con el editor preferido por el usuario. Esto es fácil de implementar usando

QProcess. Por ejemplo, asumamos que tenemos un texto plano en un QTextEdit, y proveemos un botón

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 vacío. No especificamos ningún 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 función estática QProcess::execute() ejecuta un proceso externo y bloquea la aplicación 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).

Después que el usuario cierra el editor, el proceso finaliza y libera a la aplicación. Ahora podemos abrir el

archivo temporal y cargar su contenido dentro del QTextEdit. QTemporaryFile elimina

automáticamente el archivo creado cuando el objeto sale del ámbito.

Cuando usamos QProcess síncronamente, no necesitamos realizar conexiones entre señales y slots. Si

necesitamos un control más fino que el que provee la función execute() podemos usar una técnica

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 documentación de referencia de QProcess para obtener un

ejemplo que ilustra el uso de esta técnica.

En esta sección, 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

interés para el propósito de nuestra aplicación. Otra manera de acceder a funcionalidades pre-existentes es

vincular nuestro programa con una librería que la provea. Pero donde no es factible que exista una librería,

envolver una aplicación de consola con QProcess puede funcionar muy bien.

138 12. Entrada/Salida

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 comunicación entre varias aplicaciones

en vez de ejecutarlas, podría ser mejor comunicarse directamente con ellas, usando las clases de trabajo en

red de Qt o la extensión ActiveQt si estamos trabajando en Windows.

139 13. Bases de Datos

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 integración con bases de datos a la interfaz de usuario. Este

capítulo está vinculado con las clases de modelo/vista de Qt cubierto en el Capítulo 10.

Una conexión de base de datos es representada a través de un objeto QSqlDatabase. Qt usa controladores

para comunicarse con las distintas APIs de bases de datos. La Edición de Escritorio de Qt incluye los

siguientes controladores:

Debido a las restricciones que imponen las licencias, no todos los controladores son proporcionados por la

edición 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 público.

Para los usuarios que se sienten cómodos 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 más 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

código, o pueden ser adjuntadas a vistas a través de las cuales los usuarios finales pueden ver y editar los

datos ellos mismos.

Qt también hace fácil la programación en los mosdismos de bases de datos más comunes, como el master-

detail y el dril-down, como lo demostrarán algunos ejemplos en este capítulo.

140 13. Bases de Datos

Conectando y Consultando

Para ejecutar una consulta SQL, primero debemos establecer una conexión con la base de datos. Como es

típico, las conexiones a las bases de datos son establecidas en una función separada que llamaremos al inicio

de la aplicación. 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 cuál 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 contraseña, para luego abrir la conexión. Si open() falla, mostraremos un mensaje

de error.

Típicamente, llamaríamos a crearConexion() en la función main():

int main(int argc, char *argv[])

{

QApplication app(argc, argv);

if (!crearConexion())

return 1;

•••

return app.exec();

}

Una vez que una conexión 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, año FROM cd WHERE año >= 1998");

Después del llamado a exec(), podemos navegar a través del conjunto de resultados de la consulta:

while (query.next()) {

QString titulo = query.value(0).toString();

int año = query.value(1).toInt();

cerr << qPrintable(titulo) << ": " << año << 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á vacío (o si la consulta falla),

el primer llamado a next() retornará false.

141 13. Bases de Datos

La función 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 través 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 más consumidoras de memoria que el método next(). Para una fácil

optimización cuando se trabaja con un número 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 también

podemos pasárselo directamente al constructor, el cual lo ejecuta inmediatamente:

QSqlQuery query("SELECT titulo, año FROM cd WHERE año >= 1998");

Podemos verificar si ocurrió algún 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 ningún error, el query se volverá “activo” y podremos usar next() para navegar a través de los

resultados.

Hacer un INSERT es casi tan fácil como realizar un SELECT:

QSqlQuery query("INSERT INTO cd (id, artistaid, titulo, año) "

"VALUES (203, 102, ‟Living in America‟, 2002)");

Después de esto, numRowsAffected() retorna el número 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 conversión 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, simulándolo 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, año) "

"VALUES (:id, :artistaid, :titulo, :año)");

query.bindValue(":id", 203);

query.bindValue(":artistaid", 102);

query.bindValue(":titulo", "Living in America");

query.bindValue(":año", 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, año) "

"VALUES (?, ?, ?, ?)");

query.addBindValue(203);

query.addBindValue(102);

query.addBindValue("Living in America");

query.addBindValue(2002);

query.exec();

142 13. Bases de Datos

Después 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

codificación apropiada transparentemente.

Qt soporta transacciones SQL sobre las bases de datos donde éstas estén disponibles. Para iniciar una

transacción, llamamos al método transaction() sobre el objeto que represente la conexión a la base de

datos. Para finalizar la transacción, llamamos a commit() o a rollback(). Por ejemplo, aquí se muestra

cómo haríamos para buscar una clave foránea y ejecutar una sentencia INSERT dentro de una transacción:

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, año) "

"VALUES (201, " + QString::number(artistaId)

+ ", ‟Riding the Tiger‟, 1997)");

}

QSqlDatabase::database().commit();

La función QSqlDatabase::database() retorna un objeto QSqlDatabase representando la

conexión que hemos creado en crearConexion(). Si la transacción 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 características 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 aplicación está usado una sola conexión

a la base de datos. Si queremos crear múltiples 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 al objeto QSqlDatabase pasándole el nombre a

QSqlDatabase::database():

QSqlDatabase db = QSqlDatabase::database("OTRA");

Para ejecutar consultas usando la otra conexión, pasamos el objeto QSqlDatabase al constructor de

QSqlQuery:

QSqlQuery query(db);

query.exec("SELECT id FROM artista WHERE nombre = ‟Mando Diao‟");

143 13. Bases de Datos

Múltiples conexiones son útiles si queremos realizar más de una transacción a la vez, ya que cada conexión

solamente puede manejar una sola transacción activa. Cuando usamos múltiples conexiones a bases de datos,

todavía podemos tener una conexión sin nombre, y QSqlQuery usará esa conexión si ninguna conexión es

especificada.

Adicionalmente a QSqlQuery, Qt proporciona la clase QSqlTableModel como una interfaz de más alto

nivel, permitiéndonos evitar el uso de SQL crudo para realizar la operaciones SQL más comunes (SELECT,

INSERT, UPDATE y DELETE). La clase puede ser usada de manera stand-alone (autónoma o

independiente) para manipular una base de datos sin participación de ninguna IGU (Interfaz Gráfica de

Usuario o GUI en inglés), 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("año >= 1998");

modelo.select();

este es el equivalente a la consulta

SELECT * FROM cd WHERE año >= 1998

Navegar a través del conjunto de resultados se hace recuperando un registro dado usando el método

QSqlTableModel::record() y accediendo a los campos individuales usando el método value():

for (int i = 0; i < modelo.rowCount(); ++i) {

QSqlRecord registro = modelo.record(i);

QString titulo = registro.value("titulo").toString();

int año = regitro.value("año").toInt();

cerr << qPrintable(titulo) << ": " << año << endl;

}

La función QSqlRecord::value() toma también 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 indiceAño = modelo.record().indexOf("año");

for (int i = 0; i < modelo.rowCount(); ++i) {

QSqlRecord registro = modelo.record(i);

QString titulo = registro.value(indiceTitulo).toString();

int año = registro.value(indiceAño).toInt();

cerr << qPrintable(titulo) << ": " << año << endl;

}

Para insertar un registro en una tabla de la base de datos, usamos el mismo método que usaríamos si

insertáramos en cualquier modelo bidimensional: Primero, llamamos a insertRow() para crear una nueva

fila vacía (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, 0), 113);

modelo.setData(modelo.index(fila, 1), "Shanghai My Heart");

modelo.setData(modelo.index(fila, 2), 224);

modelo.setData(modelo.index(fila, 3), 2003);

modelo.submitAll();

144 13. Bases de Datos

Después de llamar a submitAll(), el registro puede ser movido a una posición de fila diferente,

dependiendo de cómo esté ordenada la tabla. El llamado a submitAll() retornará false si la inserción

falla.

Una diferencia importante entre un modelo SQL y un modelo estándar 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("año", registro.value("año").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.

También es posible realizar una actualización usando setData(), como si lo haríamos 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 número de la fila del primer registro a eliminar y el número de

registros a eliminar. El siguiente ejemplo elimina todos los registros que coinciden con el filtro:

modelo.setTable("cd");

modelo.setFilter("año < 1990");

modelo.select();

if (modelo.rowCount() > 0) {

modelo.removeRows(0, modelo.rowCount());

modelo.submitAll();

145 13. Bases de Datos

}

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

sección y en la siguiente, presentamos una sencilla aplicación 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 aplicación usa tres tablas, mostradas en la Figura 13.2 y definidas como se muestra a continuación:

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,

año 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 foráneas. 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 sección, 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 definición 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:

146 13. Bases de Datos

enum {

Id_Artista = 0,

Nombre_Artista = 1,

Pais_Artista = 2

};

QSqlTableModel *modelo;

QTableView *tableView;

QPushButton *botonAgregar;

QPushButton *botonEliminar;

QPushButton *botonCerrar;

};

Figura 13.1. La aplicación CD Collection

Figura 13.2. Tablas de la aplicación CD Collection

Figura 13.3. El dialogo ArtistForm

147 13. Bases de Datos

El constructor es muy similar a uno que se usaría 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 creación 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 serán 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 código 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 vacía al fondo del QTableView. Ahora el usuario

puede ingresar un nuevo nombre de artista y un nuevo nombre de país. Si el usuario confirma la inserción

148 13. Bases de Datos

presionando Enter, la señal 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 señal 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 método 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 rápida (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 función generarId() solamente puede garantizar su buen funcionamiento si es ejecutada dentro del

contexto de la misma transacción 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 operación.

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 colección.

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 colección.")

.arg(registro.value("nombre").toString()));

}

}

Si hay un registro seleccionado, verificamos para ver si el artista posee algún CD, y si no lo tiene, lo

eliminamos inmediatamente. De otra forma, mostramos un mensaje explicando por qué la eliminación no fue

realizada. Estrictamente hablando, debimos haber usado una transacción, porque como se encuentra el

código, es posible para un CD tener su artista establecido al que estamos eliminando en-entre los llamados a

149 13. Bases de Datos

cdModel.select() y modelo->removeRow(). Mostraremos una transacción en la siguiente sección

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 aplicación 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_Año = 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 foráneas. Ahora revisaremos cada función en turno, comenzando

con el constructor, el cual veremos por secciones porque es algo largo.

MainForm::MainForm()

{

cdModel = new QSqlRelationalTableModel(this);

150 13. Bases de Datos

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_Año, Qt::Horizontal, tr("Año"));

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 foránea 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 automáticamente 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 foránea 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 están ocultados en el slot cdActualCambiado() mostrado más

adelante). El único aspecto notable de esta parte del código 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 creación, conexión y ubicación 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 &,

151 13. Bases de Datos

int,int)),

this, SLOT(refrescarCabeceraVistaPista()));

•••

}

La primera conexión es inusual, ya que en vez de conectar un widget, conectamos a un modelo de selección.

La clase QItemSelectionModel es usada para mantener las selecciones de pistas en vistas. Por ser

conectado al modelo de selección 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 función

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_Año),

QDate::currentDate().year());

QModelIndex indice = cdModel->index(fila, Cd_Titulo);

cdTableView->setCurrentIndex(indice);

cdTableView->edit(indice);

}

Cuando el usuario haga clic en el botón agregar CD, una nueva fila en blanco es insertada en el

cdTableView y se entra en modo de edición. También establecemos un valor por defecto para el campo

año. 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 automáticamente proporcionada por el

QSqlRelationalTableModel gracias al llamado a setRelation(), y edita el año si el que se

proporcionó por defecto no era apropiado. Si el usuario confirma la inserción presionando Enter, el registro

es insertado. El usuario puede cancelar presionando Escape.

void MainForm::antesDeInsertarCd(QSqlRecord &registro)

{

registro.setValue("id", generarId("cd"));

}

152 13. Bases de Datos

Este slot es llamado cuando el cdModel emite su señal 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 transacción, e idealmente el mecanismo específico para crear IDs de la base

de datos usada (por ejemplo, IDs auto generados) debe ser usado en lugar del método 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 botón Eliminar Cd, este slot es llamado. Si hay un CD actual, encontramos

cuántas pistas tiene éste. Si existe al menos una pista, le preguntamos al usuario que confirme la eliminación,

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 transacción, así que la eliminación 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

fácilmente editables usando un QTimeEdit.

void MainForm::agregarPista()

{

if (!cdTableView->currentIndex().isValid())

return;

153 13. Bases de Datos

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 inserción inicializada por agregarPista(), esta función 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 botón Eliminar Pista, eliminamos la pista sin formalidad alguna. Sería más fácil

usar un mensaje con las opciones Si/No si preferimos las eliminaciones previa confirmación 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 sólo si, existen pistas a ser mostradas. Este

también 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();

}

154 13. Bases de Datos

Este slot es llamado si el usuario hace clic en el botón Editar Artistas. Este proporciona dril-down en el

artista del CD actual, invocando el ArtistForm cubierto en la sección anterior y seleccionando el artista

apropiado. Si no existen registros actuales, un registro vacío seguro es retornado por record(), y esto,

inofensivamente, no encontrará coincidencias de (y por lo tanto no selecciona) ningún 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 vacía si el registro está vacío). 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 podría haber sido cambiado por

el usuario en el dialogo ArtistForm.

Para proyectos que usen clases SQL, debemos agregar la siguiente línea

QT += sql

A los archivos .pro; esto asegurará que la aplicación sea enlazada nuevamente con la librería QtSql.

Este capítulo ha mostrado que las clases de modelo/vista de Qt hacen que la visualización y la edición de

datos en base de datos SQL sea tan fácil como sea posible. En casos donde las claves foráneas refieran a

tablas con demasiados registros (digamos, unos mil o más), es probablemente mejor crear nuestro propio

delegado y usarlo para presentar un formulario de “lista de valores” con capacidad de búsqueda 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 interacción 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 código.

155 14. Redes

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 fáciles 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 también 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 conexiónes que opera en

términos de flujos de datos (data streams) transmitidos entre los nodos de red, mientras que UDP es un

protocolo sin conexión 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 también 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 más comunes y permitirnos ejecutar comandos FTP arbitrarios.

La clase QFtp trabaja asíncronamente. Cuando llamamos a una función 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 cómo recuperar un archivo usando la función get(). El

ejemplo es una aplicación de consola llamada ftpget que descarga el archivo remoto especificado en la

línea de comando. Comencemos con la función 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;

156 14. Redes

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 librería QtGui (ya que esta es una aplicación de consola). La función

QCoreApplication::arguments() retorna los argumentos de la línea 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 corazón de la función main() es la construcción del objeto FtpGet y

el llamado a la función 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 continuación:

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 función pública, 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 también posee un slot privado. Entonces conectamos la señal QFtp::done(bool) a

nuestro slot privado ftpHecho(bool). QFtp emite la señal done(bool) cuando este ha finalizado la

descarga del archivo. También tiene dos variables privadas: La variable ftp, de tipo QFtp, encapsula la

conexión 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 señal QFtp::done(bool) al slot privado ftpHecho(bool). QFtp

emite la señal done(bool) cuando se ha finalizado el procesamiento de todas las solicitudes. El parámetro

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;

157 14. Redes

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 función obtenerArchivo() empieza con la revisión de la URL que le fue pasada. Si se encuentra un

problema, la función 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 número del puerto especificado en la URL, o el puerto 21 si no se especificó en

la URL. Como ningún nombre de usuario o contraseña han sido dados a la función login(), se intenta

iniciar un login anónimo. 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 señal

done(bool) de QFtp indica la completación de todos los comandos. Esta señal 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

señal done(). Puede parecer extraño que cerremos el archivo aquí, en lugar de cerrarlo después del

llamados a ftp.close() al final de la función obtenerArchivo(), pero recordemos que los

comandos FTP son jecutados asíncronamente y pueden seguir perfectamente en proceso aun después que la

158 14. Redes

función obtenerArchivo() retorne. Solo cuando la señal 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 ejecución de un comando FTP y retornan un número

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 señal commandStarted(int) cuando se inicia la ejecución de un comando, y este emite

la señal commandFinished(int, bool) cuando la ejecución del comando es finalizada. El parámetro

int es el numero de ID que identifica al comando. Si estamos interesados en el destino de comandos

individuales, podemos guardar los números de ID cuando se programen los comandos. Seguir el rastro de los

números 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 señal stateChanged() de QFtp, la cual es

emitida si la conexión cambia de estado (QFtp::Connecting, QFtp::Connected,

QFtp::LoggedIn, etc.).

En la mayoría 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 señal

done(bool), que se emite si la cola de comandos se queda vacía.

Cuando ocurre un error, QFtp limpia la cola de comandos automáticamente. Esto quiere decir que si la

conexión o el login fallan, los comandos que sigan en la cola no serán ejecutados. Si programamos la

ejecución de nuevos comandos después de que ocurre un error usando el mismo objeto QFtp, estos

comandos serán puestos en cola y ejecutados.

En el archivo .pro de la aplicación, necesitamos la siguiente línea para enlazar con la librería QtNetwork:

QT += network

Ahora vamos a examinar un ejemplo algo más avanzado. El programa de línea de comando llamado

spider descarga todos los archivos localizados en un directorio FTP. Recursivamente va descargando

todos los subdirectorios del directorio inicial. La lógica de red está localizada en la clase spider:

159 14. Redes

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 como un QUrl y se establece usando la función

obtenerDirectorio().

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 señal listInfo(const QUrlInfo &) es emitida

por QFtp cuando solicitamos un listado de directorios (en obtenerDirectorio()) para cada archivo

que se recupera. Esta señal 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;

}

160 14. Redes

Cuando la función obtenerDirectorio() es llamada, esta hace varias revisiones de integridad y si todo

está bien, intenta establecer una conexión FTP. Esta mantiene el rastro de las rutas (paths) que debe procesar

y llama a procesarSiguienteDir() para empezar a descargar desde el directorio raíz.

void Spider::procesarSiguienteDir()

{

if (!directoriosPendientes.isEmpty()) {

directorioActual = directoriosPendientes.takeFirst();

directorioLocalActual = "downloads/" + directorioActual;

QDir(".").mkpath(directorioLocalActual);

ftp.cd(directorioActual);

ftp.list();

} else {

emit done();

}

} La función 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 función list() procese, emite un señal listInfo() que hace que el slot

ftpInfoLista() sea llamado.

Si ya no hay mas directorios por procesar, la función emite la señal 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 parámetro urlInfo del slot ftpInfoLista() provee información detallada acerca de un archivo

remoto. Si el archivo es un archivo normal (y no un directorio) y además es legible, llamamos a get() para

descargarlo. El objeto QFile usado para descargar es localizado usando un puntero hacia su ubicación en la

lista archivosArbiertos.

Si el objeto QUrlInfo contiene los detalles de un directorio remoto que no es un enlace simbólico,

agregamos este directorio a la lista directoriosPendientes. Tenemos que obviar los enlaces

simbólicos porque estos pueden inducir a una recursión infinita.

void Spider::ftpHecho(bool error)

{

if (error) {

161 14. Redes

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 también debemos cerrar

cada archivo que fue abierto. Finalmente, llamamos a procesarSiguienteDir(). Si existe algún

direcotorio restante, el proceso entero comenzará nuevamente con el directorio que siga en la lista; de otra

manera, la descarga parará y la señal done() será emitida.

Si no hay errores, la secuencia de comandos FTP y de la emisión de las señales 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 serán descargados. Si queremos

descargar tantos archivos como sea posible, una solución podría ser programar las operaciones GET una a la

vez y esperar por la señal done(bool) antes de programar la siguiente. En listInfo(), simplemente

anexaríamos 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 ejecución sería como esta:

connectToHost(host, puerto)

login()

162 14. Redes

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 solución podría ser usar un objeto QFtp por archivo. Esto nos permitiría descargar los archivos en

paralelo, a través 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 función main() completa el programa. Si el usuario no especifica una URL en la línea de comandos,

mandamos un mensaje de error y terminamos la ejecución 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);

163 14. Redes

ftp.get(urlInfo.name(), buffer);

Tambien podríamos omitir el argumento buffer a get() o pasar un puntero nulo. La clase QFtp emitirá

luego una señal readyRead() cada vez que nuevos datos estén 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 más comunes, incluyendo get() y post(), y proporciona

unos métodos para enviar solicitudes HTTP. Si has leído la sección anterior acerca de QFtp, te darás cuenta

que existen muchas similitudes entre las clases QFtp y QHttp.

La clase QHttp trabaja asíncronamente. Cuando llamamos a una función como get() o post(), la

función retorna inmediatamente, y la transferencia de datos se realiza después, cuando el control vuelva al

ciclo de eventos de Qt. Esto asegura que la interfaz de usuarios de la aplicación permanezca activa mientras

se procesan solicitudes de tipo HTTP.

Ahora vamos a examinar una aplicación de consola como ejemplo que es llamada httpget, que muestra

cómo descargar un archivo usando el protocolo HTTP. Es muy similar al ejemplo ftpget, mostrado en la

sección anterior, tanto en funcionalidad como en implementación, 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 señal 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;

}

164 14. Redes

http.setHost(url.host(), url.port(80));

http.get(url.path(), &archivo);

http.close();

return true;

}

La función obtenerArchivo() realiza el mismo tipo de comprobaciones de errores que se realizaron en

QFtp::obtenerArchivo, mostrada anteriormente, y usa el mismo método para darle al archivo un

nombre local. Cuando recuperamos datos desde sitios web, ningún 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 asíncronamente en el ciclo de eventos de Qt. La

completacion de las solicitudes se indica por medio de la señal 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, notificándole al usuario si ha ocurrido

un error.

La función 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

contraseña. 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():

165 14. Redes

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 más control, podemos usar la función 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 señal requestStarted(int) cuando comienza a ejecutar una solicitud, y emite la

señal requestFinished(int, bool) cuando la solicitud ha finalizado. El parámetro int es un

numero de ID que identifica a la solicitud. Si estamos interesados en el curso que siguen solicitudes

individuales, podemos guardar esos números ID cuando programemos las solicitudes. Siguiendo el rastro de

los números de ID, podremos proporcionar un feedback con información detallada al usuario.

En la mayoría de las aplicaciones, solamente queremos saber si toda la secuencia de solicitudes es

completada satisfactoriamente o no. Esto es fácil de hacer a través de la conexión de la señal done(bool),

la cual es emitida cuando la cola de solicitudes se queda vacia.

Cuando un ocurre un error, La cola de solicitudes es automáticamente limpiada. Pero si programamos nuevas

solicitudes después de que haya ocurrido un error usando el mismo objeto QHttp, dichas solicitudes serán

puestas en cola y eviandas, como es usual.

Al igual que QFtp, QHttp provee una señal readyRead() y también 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 mayoría de los niveles de aplicación de los

protocolos de internet, incluyendo FTP y HTTP, y puede ser usado también 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 líneas como orientados a bloques.

Los protocolos orientados a líneas transfieren los datos como líneas de texto, cada una terminada

por una nueva línea.

Los protocolos orientados a bloques tranfieren los datos como bloques de datos binarios. Cada

bloque consta de una campo tamaño seguido de los datos en si.

QTcpSocket hereda de QIODevice a través de QAbstractSocket, de manera que puede ser leído 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 sección, vamos a revisar el código 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 próximo viaje en tren. El

servidor es llamado Servidor de Viaje y provee la información de viaje al cliente. Comenzaremos con la

escritura del cliente Planeador de Viaje.

166 14. Redes

El Planeador de Viaje provee un campo Desde, un campo Hacia, un campo Fecha y un campo Tiempo

Aproximado, además de dos radio buttons para seleccionar si el tiempo aproximado es de partida o de

llegada. Cuando el usuario hace click en Buscar, la aplicación envía 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 más baja de la ventana está ocupada por un

QLabel que muestra el estado de la última operación y un QProgressBar.

Figura 14.1. La aplicación 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 código fuente de la subclase de QDialog que

implementa la funcionalidad de la aplicación:

#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 siguienteTamañoBloque;

};

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 conexión

TCP. La variable siguienteTamañoBloque es usada cuando se analizan los bloques recibidos desde el

servidor.

PlaneadorViaje::PlaneadorViaje(QWidget *parent)

: QDialog(parent)

{

setupUi(this);

167 14. Redes

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.

También escondemos la barra de progreso, porque solo queremos que aparezca cuando una conexión es

activada. En Qt Designer, las propiedades minimun y máximum 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 estándar basada en porcentajes.

En el constructor también conectamos las señales 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();

siguienteTamañoBloque = 0;

}

El slot conectarAServidor() es ejecutado cuando el usuario hace click en Buscar para empezar una

búsqueda. 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 máquina, reemplaza el nombre del servidor con

QHostAddress::LocalHost). El llamado a connectToHost() es asíncrono; siempre retorna

inmediatamente. La conexión se establece, generalmente, un tiempo después. El objeto QTcpSocket emite

la señal connected() cuando la conexión se establece y se encuentra activa., o emite la señal de error

(QAbstractSocket::SocketError) si la conexión falla.

Ahora, podemos actualizar la interfaz de usuario, haciendo la barra de progreso visible.

168 14. Redes

Finalmente, hacemos a la variable siguienteTamañoBloque igual a cero. Esta variable guarda el

tamaño del siguiente bloque recibido desde el servidor. Hemos elegido usar el valor de 0 para indicar que no

sabemos todavía el tamaño 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 señal

connected(), indicando que una conexión ha sido establecida. La tarea del slot es generar una solicitud al

servidor, con toda la información introducida por el usuario.

La solicitud es un bloque binario con el siguiente formato:

quint16 Tamaño del bloque en bytes (excluyendo este campo)

quint8 Tipo de solicitud (siempre „S‟)

QString Ciudad de partida

QString Ciudad de destino

QDate Fecha del viaje

QTime Tiempo aproximado del viaje

quint8 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 tamaño que tendrá el bloque de datos, el cual

debe ser enviado primero, hasta después que hayamos puesto todos los datos en el bloque.

Inicialmente hacemos cero al tamaño 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 tamaño de los datos del bloque. El

tamaño es calculado tomando el tamaño del bloque y restando el resultado de sizeof(quint16) (que es

2) para excluir el campo tamaño 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 {

169 14. Redes

int fila = tableWidget->rowCount();

if (siguienteTamañoBloque == 0) {

if (tcpSocket.bytesAvailable() < sizeof(quint16))

break;

entrada >> siguienteTamañoBloque ;

}

if (siguienteTamañoBloque == 0xFFFF) {

cerrarConexion();

labelEstado->setText(tr("Viajes Encontrados")

.arg(fila));

break;

}

if (tcpSocket.bytesAvailable() < siguienteTamañoBloque )

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]));

siguienteTamañoBloque = 0;

}

}

El slot actualizarTableWidget() está conectado a la señal readyRead() de QTcpSocket, el

cual es emitido si el QTcpSocket ha recibido nuevos datos desde el servidor. El servidor nos envía 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 tamaño. El ciclo forever es necesario porque no

obtenemos un bloque de datos a la vez desde el servidor necesariamente.* Tal vez podríamos 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 (;;)

170 14. Redes

¿Y cómo funciona el ciclo forever? Si la variable siguienteTamañoBloque es 0, quiere decir que

no hemos leído el tamaño 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 tamaño de 0xFFFF para indicar que

no hay más datos a ser recibidos, de manera que si leemos este valor, sabremos que hemos alcanzado el final.

Si el tamaño del bloque de datos no es 0xFFFF, tratamos de leer el siguiente bloque. Primero, verificamos

para ver si existen bytes de tamaño de bloque para ser leidos. Si no existe ninguno, paramos allí mientras

tanto con el comando break. La señal readyRead() será emitida nuevamente cuando estén disponibles

más 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 información relativa a un viaje, y creamos varios

QTableWidgetItem con esa información. Un bloque recibido desde el servidor posee el siguiente

formato:

quint16 Tamaño del bloque en bytes (excluyendo este campo)

QDate Fecha de salida

QTime Tiempo de salida

quin16 Duracion (en minutos)

quint8 Numero de cambios

QString Tipo de tren

Al final, reestablecemos la variable siguienteTamañoBloque a cero para indicar que el siguiente

tamaño del bloque es desconocido y necesita ser leído.

void PlaneadorViaje::cerrarConexion()

{

tcpSocket.close();

botonBuscar->setEnabled(true);

botonParar->setEnabled(false);

progressBar->hide();

}

La funcion privada cerrarConexion() cierra la conexión con el servidor TCP y actualiza la interfaz de

usuario. Esta es llamada desde el slot actualizarTableWidget() cuando el valor 0xFFFF es leído y

también 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 señal clicked() del botón Parar. En esencia, este slot

solamente se encarga de llamar a la función cerrarConexion().

void PlaneadorViaje::ConexionCerradaPorServidor()

{

if (siguienteTamañoBloque != 0xFFFF)

labelEstado->setText(

tr("Error: Conexión cerrada por el servidor"));

cerrarConexion();

}

El slot conexionCerradaPorServidor() está conectado con la señal disconnected() de la clase

QTcpSocket. Si el servidor cierra la conexión y aun no hemos recibido el marcador de fin 0xFFFF, le

171 14. Redes

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 señal error(QAbstractSocket::SocketError)

de QTcpSocket. Ignoramos el código 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 función 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 conexión. Existen tantos objetos ClienteSocket en memoria como número clientes siendo servidos.

class ServidorViaje : public QTcpServer

{

Q_OBJECT

public:

ServidorViaje(QObject *parent = 0);

private:

void conexionEntrante(int socketId);

};

La clase ServidorViaje reimplementa la función incomingConnection() de QTcpServer, pero

aquí la llamamos conexionEntrante(). Esta función 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 número proporcionado por nosotros. El objeto

ClienteSocket se borrará a si mismo automáticamente cuando la conexión se termine.

class ClienteSocket : public QTcpSocket

{

172 14. Redes

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 siguienteTamañoBloque;

};

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()));

siguienteTamañoBloque = 0;

}

En el constructor, establecemos las conexiones necesarias, y hacemos que la variable

siguienteTamañoBloque sea igual a cero para indicar que todavía no sabemos el tamaño del bloque de

datos que ha sido enviado por el cliente.

La señal disconnected() está conectada con deleteLayer, un objeto una función 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 conexión sea cerrado.

void ClienteSocket::leerCliente()

{

QDataStream entrada(this);

entrada.setVersion(QDataStream::Qt_4_1);

if (siguienteTamañoBloque == 0) {

if (bytesAvailable() < sizeof(quint16))

return;

entrada >> siguienteTamañoBloque;

}

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);

}

173 14. Redes

close();

}

El slot leerCliente() está conectado a la señal readyRead() de QTcpSocket. Si la variable

siguienteTamañoBloque es igual a 0, empezamos con la lectura del tamaño del bloque; de otra forma,

quiere decir que ya lo hemos leído, y en lugar de calcularlo verificamos si el bloque ha llegado completo.

Una vez que un bloque completo se encuentra listo para ser leído, lo leemos. Usamos el QDataStream

directamente en el QTcpSocket (el objeto this) y leemos los archivos usando el operador >>.

Una vez que hayamos leído la solicitud del cliente, estaremos listos para generar una respuesta. Si esta fuera

una aplicación real, buscaríamos la información en una respectiva base de datos y trataríamos de encontrar

viajes de trenes. Pero aquí nos será suficiente con una función llamada generarViajeAleatorio()

que generará un viaje aleatorio. Llamamos a la función un número aleatorio de veces, y luego enviamos el

valor 0xFFFF para indicar el final de los datos. Al final, cerramos la conexión.

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 función generarViajeAleatorio() muestra cómo enviar un bloque de datos sobre conexiones

TCP. Esto es muy similar a lo que hicimos en el cliente en la función enviarSolicitud(). Una vez

más, escribimos el bloque de datos a un QByteArray para que podamos determinar su tamaño antes de

enviarlo usando la función 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 dirección

IP y el número del puerto en el cual queremos aceptar conecciones. La dirección 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 queríamos era

usar un protocolo orientado a líneas, el método más simple podría haber sido usar las funciones

174 14. Redes

canReadLine() y readLine() de la clase QTcpSocket en un slot conectado a la señal

readyRead():

QStringList lineas;

while (tcpSocket.canReadLine())

lineas.append(tcpSocket.readLine());

Luego procesaríamos cada línea que ha sido leida. En lo que respecta a el envio de datos, este puede hacerse

usando un QTextStream en el QTcpSocket.

La implementación 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 más escalable sería iniciar un nuevo hilo para cada conexión. El ejemplo

Threaded Fortune Server ubicado en el directorio examples/network/threadedfortuneserver de Qt se

ilustra cómo 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 más ligero que

TCP. Con UDP, los datos son enviados como paquetes (datagramas) de un host a otro. Allí, no existe el

concepto de conexión, y si un paquete UDP no es entregado satisfactoriamente, ningún error es reportado al

remitente.

Veremos cómo usar UDP desde una aplicación Qt a través de los ejemplos Globo Climatológico y Estación

Climatológica. La aplicación Globo Climatológico imita a un globo climatológico que envía datagramas

UDP (presumiblemente usando una conexión inalámbrica) cada 2 segundos conteniendo las condiciones

atmosféricas. La aplicación Estación Climatológica recibe estos datagramas y los muestra en pantalla.

Primero, comenzaremos revisando el código de la aplicación Globo Climátológico.

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 Estación Climatológica (la otra aplicación).

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 Climatológico"));

}

175 14. Redes

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 Fecha y hora de medición

double Temperatura en ºC

double Humedad en %

double Altitud en metros

El datagrama es enviado usando QUdpSocket::writeDatagram(). El segundo y tercer argumento a

writeDatagram() son la dirección IP y el numero del puerto del peer (la Estación Climatológica)

respectivamente. Para este ejemplo, asumimos que la Estación Climatológica está corriendo en la misma

máquina que Globo Climatológico, así que usamos una IP de 127.0.0.1 (QHostAddress::LocalHost),

una dirección especial que designa el host local.

A diferencia de las subclases de QAbstractSocket, QUdpSocket no acepta nombres de host, solo

direcciones de host. Si queríamos determinar la dirección IP a partir del nombre de host, tenemos dos

opciones: si estamos preparados para bloquear la interfaz mientras la búsqueda se hace, podemos usar la

función estática QHostInfo::fromName(). De otra manera, podemos usar la función estática

QHostInfo::lookupHost(), la cual retorna inmediatamente y llama al slot que le es pasado con un

objeto QHostInfo conteniendo las direcciones correspondientes cuando la búsqueda esté completa.

int main(int argc, char *argv[])

{

QApplication app(argc, argv);

GloboClimatologico globo;

globo.show();

return app.exec();

}

La función 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

aplicación.

Ahora revisemos el código fuente para el cliente Estación Climatológica.

class EstacionClimatologica : public QDialog

{

Q_OBJECT

public:

EstacionClimatologica(QWidget *parent = 0);

private slots:

void procesarDatagramasPendientes();

private:

QUdpSocket udpSocket;

QLabel *labelFecha;

176 14. Redes

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 interés 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 aplicación Estación Climatológica

En el constructor, comenzamos con la vinculación del QUdpSocket al puerto en el que el globo

climatológico está transmitiendo. Ya que no hemos especificado una dirección de host, el socket aceptará

datagramas enviados a cualquier dirección IP que pertenezca a la máquina en donde la aplicación Estación

Climatologica se esté ejecutando. Luego, conectamos la señal 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());

177 14. Redes

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, Debería haber solo un datagrama, pero no podemos excluir la posibilidad de que el remitente

podría enviar unos cuantos datagramas en una fila antes de que la señal readyRead() sea emitida. En ese

caso, podemos ignorar todos los datagramas exceptuando al último, ya que el anterior contiene condiciones

atmosféricas obsoletas.

La función pendingDatagramSize() retorna el tamaño del primer datagrama pendiente. Desde el

punto de vista de la aplicación, 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 leído. La llamada a readDatagram() copia el contenido del primer datagrama pendiente a un

buffer char * especificado (truncando los datos si el buffer es muy pequeño) y avanza hasta el siguiente

datagrama pendiente. Una vez que hemos leído todos los datagramas, descomponemos el ultimo (el único

con las medidas atomosfericas más 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 operación de envio de datagramas que hace la aplicación Globo Climatológico y la recepción de estos

por parte de la aplicación Estación Climatológica. En la mayoría de las aplicaciones del mundo real, ambas

aplicaciones necesitarían la lectura y la escritura en su socket. A la función

QUdpSocket::writeDatagram() se le puede pasar una dirección de host y un número de puerto, para

que QUdpSocket pueda leer desde el host y el puerto recibido usando bind(), y escribir en algún otro

host y puerto.

178 15. XML

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

propósitos 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 análisis de texto”

directamente a la aplicación a través 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 aplicación.

Existen muchos factores que deben tomarse en cuenta cuando se elige entre DOM y SAX para una

aplicación en particular. SAX es de más bajo nivel y por tanto usualmente más rápido, 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 más 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 estándar, de dominio publico de hecho, para leer documentos XML. Las clases SAX de Qt

son modeladas siguiendo de la implementación de SAX2 en Java, con algunas diferencias en el nombrado

para que encajen con las convenciones de Qt. Para más información acerca de SAX, visite

http://www.saxproject.org/.

Qt proporciona un parseador (analizador sintáctico o analizador de texto) XML sin validaciones basado en

SAX llamado QXmlSimpleReader. Este analizador reconoce código 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 están relacionados con los eventos de Qt, como

los eventos de teclado y ratón). Por ejemplo, supongamos que el parseador se encuentra analizando el

siguiente documento XML:

<doc>

<quote>Ars longa vita brevis</quote>

</doc>

179 15. XML

El parseador llamaría 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 información acerca de los diferentes eventos de parseo. Para la

mayoría de las aplicaciones, QXmlContentHandler y QXmlErrorHandler son las únicas que se

necesitarán.

Por comodidad, Qt también provee la clase QXmlDefaultHandler, una clase que hereda de todos las

clases manejadoras y que provee implementaciones triviales para todas las funciones. Este diseño, con

muchas clases manejadoras abstractas y una subclase trivial, es inusual en Qt; esta fue adoptada para seguir,

lo más cerca posible, el modelo de la implementación en Java.

Veremos ahora un ejemplo que muestra cómo 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>

180 15. XML

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 información

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 función elementoInicio() es lamada cuando el lector encuentra una etiqueta de inicio. El tercer

parámetro en el nombre de la etiqueta (o más precisamente, su “nombre calificativo”). El cuarto parámetro es

la lista de atributos. En este ejemplo, ignoramos los parámetros 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 documentación 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

181 15. XML

anidadora. Si la etiqueta no está anidada, entonces creamos el QTreeWidgetItem con el QTreeWidget

como padre, haciéndolo 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 vacía. 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 queríamos reportar

errores al encontrar etiquetas desconocidas, podríamos haber retornado false para esos casos. Luego

reimplementariamos la función 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 función elementoFin() es llamada cuando el lector encuentra una etiqueta de cierre. Como sucede

con elementoInicio(), el tercer parámetro 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 función 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 línea, el numero de columna y el texto del

error producido por el parseador.

Esto completa la implementación de la clase SaxHandler. Ahora veamos cómo 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);

182 15. XML

return lector.parse(inputSource);

}

Primero, configuramos un QTreeWidget con dos columnas. Luego creamos un objeto QFile para el

archivo que va a ser leído y un QXmlSimpleReader para parsear el archivo. No necesitamos abrir el

QFile por nuestra cuenta; ya que QXmlImputSource lo hace automáticamente.

Finalmente, creamos un objeto SaxHandler, lo instalamos sobre el lector como manejador de contenido y

como manejador de errores, y llamamos a la función parse() sobre el leector para realizar el parseo.

En lugar de pasar simplemente un objeto de archivo a la función parse(), pasamos un

QXmlInputSource. Esta clase abre el archivo que se le da, lo lee (tomando en cuenta cualquier

codificación de caracteres especificada en la declaración <?xml?>), y proporciona una interfaz a través de

la cual el parseador lee el archivo.

En la clase SaxHandler, solamente reimplementamos las funciones de las clases

QXmlContentHandler y QXmlErrorHandler. Si hubiéramos implementado las funciones de otras

clases manejadoras, habriamos necesitado llamar a sus correspondientes funciones seteadoras sobre el lector.

Para enlazar la aplicación con la librería QtXml, debemos agregar esta línea al archivo .pro:

QT += xml

Leyendo XML con DOM DOM es una API estándar para el parseo (análisis gramático o sintáctico) de XML desarrollado por el World

Wide Web Consortium (W3C). Qt proporciona una implementación sin validaciones de DOM Level 2 para

la lectura, manipulación y escritura de documentos XML.

DOM representa un archivo XML como un árbol en la memoria. Podemos navegar a través 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:

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.

Document

o Element (doc)

Element (cita)

Text (“Ars longa vita brevis”)

Element (traduccion)

Text (“El arte es larga, la vida corta”)

183 15. XML

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 también nodos EntityReference, Text,

CDATASection, ProcessingInstruction y Comment. La Figura 15.3 muestra cuáles nodos

pueden tener cuáles tipos de nodos hijos. Los nodos en gris no pueden tener ningún hijo de su propio tipo.

Figura 15.3. Relacion padre-hijo entre nodos DOM

Para ilustrar cómo usar DOM para la lectura de archivos XML, escribiremos un parseador para el formato

del archivo de índice del libro descrito en la sección 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();

184 15. XML

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 método setContent() para leer

el documento XML proporcionado por el objeto QIODevice. La función setContent() abre

automáticamente 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 función toElement() retornará un objeto QDomElement nulo, con un nombre de etiqueta

vacío.

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();

}

}

185 15. XML

En la función 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, haciéndolo 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() pasándole 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 través 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

números de páginas (que se encuentran separados por comas) en la columna 1 del QTreeWidgetItem.

Ahora veamos cómo 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 línea en el archivo .pro de la aplicación para

enlazarlo con la librería QtXml:

QT += xml

Como ilustra el ejemplo, navegar a través de un árbol DOM puede ser engorroso. Simplemente extrayendo el

texto entre <pagina> y </pagina> requiere que la iteracción 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 más ncesitadas y comunes, como la

extracción del texto entre las etiquetas de inicio y de cierre.

Ecribiendo XML

Existen, básicamente, dos métodos para generar archivos XML desde una aplicación Qt:

Podemos construir un árbol DOM y llamar al método save() desde este.

Podemos generar el XML a mano.

La elección entre alguno de estos dos métodos muchas veces depende de si usamos SAX o DOM para la

lectura de documentos XML.

186 15. XML

Aquí se encuentra un fragmento de código que muestra cómo 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 tamaño de la identacion a usar. Un valor distinto de cero hace que el

archivo sea más fácil 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 leerán los archivos XML usando DOM, luego modificarán el

árbol DOM en memoria y finalmente llamarán a save() para convertir el árbol nuevamente en XML.

Por defecto, QDomDocument::save() usa la codificación UTF-8 para generar el archivo. Podemos usar

cualquier otra codificación colocando en el inicio del árbol DOM una declaración XML como esta:

<?xml version="1.0" encoding="ISO-8859-1"?>

El siguiente segmento de código nos muestra cómo 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 más difícil que usar DOM. Podemos usar QTextStream y

escribir las cadenas como lo haríamos con cualquier otro archivo de texto. La parte más tramposa y delicada

es escapar de los caracteres especiales en los valores del texto y de los atributos. La función

Qt::escape() escapa los caracteres „<‟, „>‟ y „&‟. Aquí está un código 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/qq05-generating-xml.html, presenta una clase muy simple que facilita la generación de archivos XML. Esta clase

se ocupa de los detalles tales como caracteres especiales, indentacion, y asuntos de codificación,

187 15. XML

permitiéndonos concentrarnos en el XML que queremos generar. La clase fue diseñada para trabajar con Qt

3 pero es inecesesario portarlo a Qt 4.

188 16. Proporcionando Ayuda En Linea

16. Proporcionando Ayuda En Linea

Ayudas: Tooltips, Status Tips y “What’s This?”

Usando QTextBrowser Como un Mecanismo de Ayuda

Usando Qt Assistant como una Poderosa Ayuda En Linea

La mayoría de las aplicaciones proveen a los usuarios de ayuda en línea. Algunas ayudas son cortas, como

son los tooltips, status tips y “What´s This?”. Naturalmente, Qt soporta todas estas. Otras ayudas pueden ser

mucho más extensas, involucrando varias páginas de texto. Para este tipo de ayudas, usted puede usar el

QTextBrowser como un simple buscador de ayuda en línea, o puede invocar el Qt Assistant o un

Navegador HTML desde su aplicación.

Ayudas: Tooltips, Status Tips, y “What´s This?”

Un tooltip es un pequeño 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 descripción textual de botones en una barra de herramientas.

Podemos agregar un tooltip a cualquier widget en el código usando QWidget::setToolTip(). Por

ejemplo:

botonBuscar->setToolTip(tr("Buscar siguiente"));

Para ponerle un tooltip a un QAction que pueda ser añadido a un menú o una barra de herramientas,

podemos simplemente llamar el método setToolTip() de la acción. Por ejemplo:

accionNuevo = new QAction(tr("&Nuevo"), this);

accionNuevo->setToolTip(tr("Nuevo documento"));

Si no le ponemos explícitamente un tooltip, el QAction usará el texto de la acción.

Un status tip es también un corto fragmento descriptivo de texto, usualmente un poco más largo que un

tooltip. Cuando el puntero del mouse pasa encima de un botón de una barra de herramientas o una opción de

un menú, un status tip aparece justo en la barra de estado. Para agregar un status tip a una acción o un widget

use el método setStatusTip():

accionNuevo->setStatusTip(tr("Crear un nuevo documento"));

En algunas situaciones, es deseable brindar más información acerca de un widget de la que puede ser dada

por tooltips y status tips. Por ejemplo, se podría querer mostrar un diálogo complejo con texto que explique

cada campo sin forzar al usuario a invocar una ventana de ayuda por separado. El modo “What´s This?” es la

solución ideal para esto. Cuando una ventana está en el modo “What´s 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.

189 16. Proporcionando Ayuda En Linea

Para entrar en el modo “What´s This?” el usuario puede hacer click en el botón ? en la barra de título del

diálogo (en Windows y KDE) o presionar Shift+F1.

Figura 16.1. Una aplicación mostrando un tooltip y un status tip

Este es un ejemplo de un texto “What´s This?” aplicado a un diálogo:

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 Institución "

"y un nombre de Departamento"

"</ul>"));

Podemos usar etiquetas HTML para dar formato al texto de un “What´s This?”. En el ejemplo, incluimos una

imagen (la cual estaba listada en el archivo de recursos de la aplicación), una lista con viñetas, y algún texto

en negrita. Las etiquetas y atributos que Qt soporta están especificadas en

http://doc.trolltech.com/4.1/richtext-html-subset.html. Cuando le ponemos un texto “What´s This?” a una acción, el texto será mostrado cuando el usuario haga

click en el elemento del menú o botón de la barra de herramientas o presione el método abreviado estando en

modo “What´s This?”. Cuando los componentes de la ventana principal de una aplicación proveen el texto

“What´s This?”, puede ser incluida la opción “What´s This?” en el menú de Ayuda y su correspondiente

botón en la barra de herramientas. Esto puede ser hecho creando una acción “What´s This?” usando la

función estática QWhatsThis::createAction() y agregando la acción que retorna al menú de Ayuda

y a la barra de herramientas. La clase QWhatsThis, además, presenta funciones estáticas para entrar y

salir del modo “What´s This?”.

190 16. Proporcionando Ayuda En Linea

Figura 16.2. Un dialogo mostrando un texto de ayuda “What’s This?”

Usando QTextBrowser como un Mecanismo de Ayuda

Las aplicaciones grandes pueden necesitar de más ayuda en línea que la ayuda que los tooltips, status tips y

“What´s This? pueden mostrar. Una simple solución a esto es brindar un buscador de ayuda. Las

aplicaciones que incluyen un buscador de ayuda típicamente tienen un vínculo a la Ayuda en el menú

Ayuda en la ventana principal y un botón de Ayuda en cada diálogo.

En esta sección, presentaremos un simple buscador de ayuda que se muestra en la Figura 16.3 y

explicaremos cómo puede ser usado dentro de una aplicación. La ventana usa un QTextBrowser para

mostrar las páginas de ayuda que están usando una sintaxis basada en HTML. Un QTextBrowser puede

manipular una gran cantidad de etiquetas HTML, por eso es ideal para este propósito.

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;

};

191 16. Proporcionando Ayuda En Linea

La clase BuscadorAyuda provee una función estática que puede ser llamada desde cualquier parte de la

aplicación. Esta función crea una ventana BuscadorAyuda y muestra la página dada.

Figura 16.3. El widget BuscadorAyuda

Aquí está el principio de la implementación:

#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 &)),

192 16. Proporcionando Ayuda En Linea

this, SLOT(actualizarTituloVentana()));

textoBuscador->setSearchPaths(QStringList()<< ruta<< ":/imagenes");

textoBuscador->setSource(pagina);

}

Ponemos el atributo Qt::WA_GroupLeader porque queremos mostrar la ventana BuscadorAyuda

desde diálogos modales además de la ventana principal. Los diálogos modales normalmente previenen la

interacción del usuario con cualquier otra ventana en la aplicación. Sin embargo, después de llamar la ayuda,

obviamente se le debe permitir al usuario interactuar con el diálogo modal y con el buscador de ayuda.

Activar el atributo Qt::WA_GroupLeader hace esta interacción posible.

Proveemos dos rutas de búsqueda, la primera, una ruta en el sistema de archivos que contiene la

documentación de la aplicación, y la segunda, la localización de los recursos de imágenes. El HTML puede

incluir referencias a imágenes en el sistema de archivos en la forma normal y además referencias a recursos

de imágenes usando una ruta que comienza con :/ (dos punto slash). El parámetro pagina, es el nombre

del archivo de documentación, con un ancla HTML opcional.

void BuscadorAyuda::actualizarTituloVentana()

{

setWindowTitle(tr("Ayuda: %1"). arg(textoBuscador->

documentTitle()));

}

Siempre que la página fuente cambia, el slot actualizarTituloVentana() es llamado. La función

documentTitle() retorna el texto especificado en la etiqueta de la página <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 función estática mostrarPagina(), creamos la ventana BuscadorAyuda y luego la mostramos.

La ventana será destruida automáticamente cuando el usuario la cierre, al ser puesto el atributo

Qt::WA_DeleteOnClose en el constructor de BuscadorAyuda.

Para este ejemplo, asumimos que la documentación se encuentra en el subdirectorio doc del directorio que

contiene el ejecutable de la aplicación. Todas las páginas pasadas a la función mostrarPagina() serán

tomadas desde ese subdirectorio.

Ahora estamos listos para invocar el buscador de ayuda desde la aplicación. En la ventana principal de la

aplicación, podemos crear una acción Ayuda y conectarla a un slot ayuda() que se muestra a

continuación:

void MainWindow::ayuda()

{

BuscadorAyuda::mostrarPagina("index.html");

}

Esto asume que el archivo principal de la ayuda es llamado index.html. Para los diálogos, podemos

conectar el botón Ayuda al slot ayuda() de la siguiente forma:

void DialogoEntrada::help()

{

BuscadorAyuda::mostrarPagina("forms.html#editando");

}

193 16. Proporcionando Ayuda En Linea

Aquí podemos buscar en diferentes archivos de ayuda, forms.html, y navegar en el QTextBrowser

hasta el ancla editando, que lleva al la sección de edición.

Usando el Qt Assistant como una Poderosa Ayuda En Línea

El Qt Assistant es una aplicación redistribuible de ayuda en línea suministrada por Trolltech. Sus principales

virtudes son que puede indexar, hacer búsqueda de texto y puede manejar conjuntos de documentación para

múltiples aplicaciones.

Para hacer uso del Qt Assistant, debemos incorporar el código necesario en nuestra aplicación, y debemos

responsabilizar al Qt Assistant de nuestra documentación.

La comunicación entre una aplicación de Qt y el Qt Assistant es manejada por la clase

QAssistantClient, la cual se encuentra en una librería por separado. Para enlazar esta librería con una

aplicación, debemos añadir la siguiente línea al archivo .pro de la aplicación:

CONFIG += assistant

Ahora veamos el código 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 vacía, significa que QAssistantClient

debe buscar el ejecutable en la variable de entorno PATH. QAssistantClient tiene una función

showPage() que acepta un nombre de una página con un ancla HTML opcional.

194 16. Proporcionando Ayuda En Linea

El próximo paso es preparar la tabla de contenidos y un índice para la documentación. Esto se hace creando

un perfil Qt Assistant y escribiendo un archivo .dcf que provee información acerca de la documentación.

Todo eso es explicado en la documentación en línea del Qt Assistant, así que no vamos a duplicar esa

información aquí.

Una alternativa a usar QTextBrowser o Qt Assistant es usar métodos específicos de cada plataforma para

crear ayudas en línea. 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 método adecuado sería 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 capítulos que siguen en la Parte III cubren características más

avanzadas y especializadas de Qt. El C++ y el código de Qt que se presentan allí no es más difícil que el

visto en la Parte II, pero algunos de los conceptos e ideas pueden ser más desafiantes en aquellas áreas que

sean nuevas para usted.

195

Parte III Qt Avanzado

196 17. Internacionalización

17. Internacionalización

Trabajando con Unicode

Haciendo Aplicaciones que Acepten Traducciones

Cambio Dinámico del Lenguaje

Traduciendo Aplicaciones

Adicionalmente al alfabeto Latino usado para el inglés y para muchos otros lenguajes europeos, Qt 4 también

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 aplicación puede soportar a todos los usuarios del mismo modo.

Los próximos motores de Qt pueden manejar la gran mayoría de los sistemas de escrituras no

Latinos, incluyendo el Árabe, Chino, Cirílico, Hebreo, Japonés, Coreano, Tailandés y los lenguajes

Índicos.

Los motores de Layout de Qt soportan la distribución de los elementos de la aplicación de derecha a

izquierda para lenguajes como el árabe y el hebreo.

Ciertos lenguajes requieren métodos de entrada especiales para introducir texto. Los widgets de

edición, como el QLineEdit y el QTextEdit trabajan bien con cualquier método 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 función tr() (como lo hemos hecho en capítulos anteriores) y usa las herramientas de soporte

para preparar los archivos de traducción 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

aplicación.

Para la mayoría de las aplicaciones, un archivo de traducción es cargado al iniciar, basado en las

configuraciones locales del usuario. Pero en unos cuantos casos, es también necesario para los usuarios, que

puedan cambiar el lenguaje de la aplicación en tiempo de ejecución. Esto es perfectamente posible hacerlo

con Qt, aunque requiera un poquito más de trabajo. Y gracias al sistema de Layouts de Qt, los distintos

componentes de la interfaz de usuario se ajustarán automáticamente para hacer espacio al texto traducido,

cuando este sea más largo que el texto original.

197 17. Internacionalización

Trabajando con Unicode Unicode es un estándar de codificación de caracteres que soporta la mayoría de los sistemas de escritura del

mundo. La idea original detrás de Unicode es que, se usen 16 bits para almacenar caracteres en vez de 8 bits,

eso haría 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 código. 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 Latin-

1 y en Unicode.

[*] Las versiones recientes del estándar 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 español 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 fácil:

str [0] = „Ñ‟;

Y si el archivo fuente posee otra codificación, el valor numérico funciona bien:

str [0] = QChar (0xD1);

Podemos especificar cualquier caracter Unicode por su valor numérico. Por ejemplo, aquí está cómo

especificar la letra capital griega sigma („∑‟) y el símbolo del euro („€‟):

str [0] = QChar (0x03A3);

str [0] = QChar (0x20AC);

Los valores numéricos de todos los caracteres soportados por Unicode están listados en la siguiente dirección

web: http://www.unicode.org/standar/. Si raramente necesitas caracteres que no sean Latin-1, mirar los

caracteres vía online es suficiente; pero Qt provee maneras más convenientes de introducir cadenas de texto

Unicode en un programa hecho con Qt, como veremos más tarde en esta sección.

El motor de texto de Qt 4 soporta los siguientes sistemas de escritura en todas las plataformas: aravico,

chino, cirílico, griego, hebreo, japonés, coreano, lao, latín, tailandés y vietnamita. También soporta todos los

scripts Unicode 4.1 que no requieren ningún 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, canarés, jemer, malabar, syriac, tamil, telugu, thaana (dhivelhi), y el tibetano.

Finalmente, el oriya es soportado en X11, y el mongol y el sinhala están soportados en Windows XP.

Asumiendo que los métodos apropiados están instalados, los usuarios serán 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 numérico de

un QChar, se llama a la función unicode() perteneciente a este. Para obtener el valor ASCII o Latin-1 de

un QChar (como un char), se llama a la función toLatin1(). Para caracteres que no son del sistema

Latin-1, la función toLatin1() retorna el caracter „\0‟.

Si sabemos que todas las cadenas de texto en un programa son ASCII, podemos usar las funciones estándar

de la cabecera <cctype> como la función isalpha(), isdigit(), y isspace() en el valor de

retorno de toLatin1(). Sin embargo, generalmente es bueno usar funciones miembros QChar para

198 17. Internacionalización

realizar con eficiencia estas operaciones, dado que ellas funcionarán 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

dígito o una letra mayúscula:

if(ch.isDigit()||ch.isUpper())

El código de arriba funciona para cualquier alfabeto donde se distinga entre mayúsculas y minúsculas,

incluyendo el latín, el griego y el cirílico.

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 atención 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 codificación

de un archivo de texto por su contenido. Por defecto, QTextStream usa la codificación 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 diseñamos 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 serán por lo tanto guardados en formato UTF-

16, 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 también si los

bytes están en orden llittle –endian o en orden big –endian (véase Endiannes en el Glosario de Términos). El

formato UTF-16 es idéntico a la representación de memoria de un QString, así que, leer y escribir cadenas

de texto Unicode en UTF-16, puede ser muy rápido. Por otro lado, existe una sobrecarga o saturación

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 codificación dada. Qt

usa QTextCodecs en una variedad de contextos. Internamente, son usados para dar soporte a las fuentes, a

métodos de entrada, al portapapeles, al arrastrar y soltar y en nombres de archivos. Pero están 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 automáticamente. Esta funcionalidad o comportamiento puede

ser desactivada a través de la llamada a setAutoDetectUnicode(false). Si los datos están 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 extensión a Unicode es el formato UTF-8. Su principal ventaja

sobre UTF-16 es que es un súper 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 múltiples. Para textos que son mayormente

ASCII, el formato UTF-8 ocupa aproximadamente la mitad del espacio consumido por el formato UTF-16.

199 17. Internacionalización

Para usar el formato UTF-8 con QTextStream, hay que llamar a setCodec() con “UTF-8” como el

nombre del códec antes de leer o escribir

Si siempre queremos leer y escribir en Latín-1, independientemente de la configuración regional del usuario,

podemos establecer el códec “ISO 8859-1” en el QTextStream. Por ejemplo: QTextStream in(&archivo);

in.setCodec(“ISO 8859-1”);

Algunos formatos de archivos especifican su codificación en sus cabeceras. Generalmente, la cabecera está

en ASCII puro, para asegurarse de que el archivo es leído correctamente sin importar qué codificación es

usada (asumiendo que es un súper 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 función setCodec() con “UTF-8” como argumento. Si el formato es

UTF-16, QTextStream lo detectará automáticamente y se ajustará por sí mismo. La cabecera <?xml?>

de un archivo XML algunas veces contiene un argumento de codificación, por ejemplo: <?xml versión=”1.0” encoding=”EUC-KR”?>

Ya que QTextStream no permite que modifiquemos la codificación una vez que se ha iniciado la lectura,

la forma correcta de respetar una codificación explicita es empezar a leer el archivo renovado, usando el

códec correcto (obtenido de QTextCodec::codecForName()). En el caso de XML, podemos evitar

tener que manejar nosotros mismos la codificación mediante el uso de las clases XML de Qt, que se

describen en el Capítulo 15.

Otro uso para QTextCodec es especificar la codificación de cadenas de texto que se producen en el código

fuente. Consideremos, por ejemplo, un equipo de programadores japoneses quienes se encuentran

escribiendo una aplicación dirigida, principalmente, al mercado doméstico de Japón. Estos programadores

pueden escribir su código fuente en un editor de texto que use una codificación como EUC-JP o Shift-JIS.

De manera que, un editor les permite escribir en caracteres japoneses a la perfección para que puedan escribir

código 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 función estática QTextCodec::setCodecForTr(). Por ejemplo:

QTextCodec::setCodecForTr(QTextCodec::codecForName(“EUC-JP”));

Esto debe hacerse antes de la primera llamada a tr(). Típicamente, haremos esto en el main(),

inmediatamente después de que el objeto QCoreApplication o QApplication sea creado.

Otras cadenas de texto especificadas en el programa serán interpretadas como cadenas Latin-1. Si el

programador quiere ingresar caracteres Japoneses en estas, de igual forma pueden convertirlas

explícitamente a Unicode usando un QTextCodec:

QString text = japaneseCodec->toUnicode( );

Alternativamente, también pueden decirle a Qt que use un códec especifico cuando se hace una conversión

entre const char* y QString a través de la llamada a QtextCodec::setCodecForCStrings():

QTextCodec::setCodecForCStrings(QTextCodec::codecForName("EUC-JP"));

Las técnicas 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

200 17. Internacionalización

• 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 válido. Otras

codificaciones pueden soportarse usando una subclase de QTextCodec.

201 17. Internacionalización

Haciendo Aplicaciones que Acepten Traducciones

Si queremos hacer que nuestras aplicaciones estén disponibles en múltiples lenguajes, debemos hacer dos

cosas:

Asegurarnos de que cada cadena de texto visible para el usuario pase por la función tr().

Leer un archivo de traducción (.qm) al iniciar la aplicación.

Ninguno de estos pasos es necesario para las aplicaciones que nunca necesitarán ser traducidas. Sin embargo,

usar la función tr() casi no requiere esfuerzo y deja la posibilidad de hacer traducciones a la aplicación en

otro momento.

La función tr() es una función estática definida en QObject y sobrepuesta en cada subclase definida con

el macro Q_OBJECT. Cuando se escribe código dentro de una subclase de QObject, podemos llamar a la

función tr() sin formalidad. Una llamada a tr() retorna una traducción del texto, si se encuentra

disponible; de otra forma, el texto original es retornado.

Para preparar los archivos de traducción, debemos ejecutar la herramienta de Qt lupdate. Esta

herramienta, extrae todas las cadenas de texto que aparecen en la las llamadas a la función tr() y produce

archivos de traducción que contienen todas esas cadenas de texto listas para ser traducidas. Luego, los

archivos pueden ser enviados a un traductor para tener las traducciones añadidas. Este proceso es explicado

en la sección “Traduciendo Aplicaciones”, más adelante en este capítulo.

Una llamada a la función 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 función tr() desde una función miembro de la clase en cuestión.

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 información adicional al traductor.

Aquí están 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", " tamaño 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 también 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 demás. Los

traductores trabajan típicamente con un solo contexto a la vez, a menudo con la aplicación ejecutándose y

mostrando el widget o el diálogo a ser traducido.

Cuando llamamos a tr() desde una función global, debemos especificar el contexto explícitamente.

Cualquier subclase de QObject en la aplicación puede ser usada como el contexto. Si ninguna es apropiada,

siempre podemos optar por usar el mismo QObject. Por ejemplo:

202 17. Internacionalización

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 más

general de traducir una cadena de texto en Qt, es usar la función

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 cómo escribimos nuestro código. Por ejemplo, el código 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 dinámicas.

// MAL

statusBar()->showMessage(tr("Host "+ NombreHost +" encontrado"));

Aquí, la cadena que pasamos a tr() varía dependiendo del valor de NombreHost, de manera que no

podemos esperar que tr() lo traduzca correctamente.

La solución es usar QString::arg():

statusBar()->showMessage(tr("Host %1 encontrado").arg(NombreHost));

Nótese cómo trabaja: La cadena de texto “Host %1 encontrado” es pasada a tr(). Asumiendo que se ha

cargado un archivo de traducción al francés, tr() retornaría algo como "Hôte %1 trouvé". Luego el

parámetro “%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 traducción

después de que las asignemos a una variable. Esto es útil mayormente para arreglos estáticos 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("Orquídea Calipso"),

QT_TR_NOOP("Buqué Rosas Rojas Secas"),

QT_TR_NOOP("Buqué Peonias Mixtas"),

0

};

203 17. Internacionalización

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 traducción como de costumbre. Aun cuando pasemos una

variable a tr(), la traducción seguirá funcionando.

Existe también el macro QT_TRANSLATE_NOOP() que funciona como el macro QT_TR_NOOP() pero

además 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", "Orquídea 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 función tr() en una aplicación, es muy fácil 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 aplicación traducida, cuando algunas cadenas de texto aparezcan en el

lenguaje original. Para evitar este problema, podemos decirle a Qt que prohíba conversiones implícitas de

const char * a QString. Podemos hacer esto definiendo el símbolo preprocesador

QT_NO_CAST_FROM_ASCII antes de incluir cualquier cabecera de Qt. La manera más fácil de asegurarse

de que este símbolo este establecido, es añadir la siguiente línea al archivo de proyecto (.pro) de la

aplicación:

DEFINES += QT_NO_CAST_FROM_ASCII

Esto fuerza a cada cadena textual a ser envuelta por la función tr() o por QLatin1String(),

dependiendo de si ésta debe ser traducida. Las cadenas que no son debidamente envueltas producirán un

error de compilación, y esto, por consecuencia, nos obligará a añadir 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 traducción, es cargar un archivo de traducción. Típicamente, haríamos esto en

la función main() de la aplicación. Por ejemplo, aquí puede apreciarse cómo se trataría de cargar un

archivo de traducción 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 función QLocale::system() retorna un objeto QLocale que proporciona información 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 más o menos precisos; por ejemplo, fr especifica

una localidad de lenguaje francés, fr_CA especifica una localidad Francesa-Canadiense y

204 17. Internacionalización

fr_CA.ISO8859-15 especifica una localidad francesa con la codificación ISO 8859-15 (una codificación

que soporta los caracteres „€‟,‟ 'Œ’,’œ’ y ‘Ÿ’).

Suponiendo que la localidad es fr_CA.ISO8859-15, la función QTranslator::load() trata primero de

leer el archivo miApp_fr_CA.ISO8859-15.qm. Si este archivo no existe, la función 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 proporcionaríamos el archivo miApp.qm, conteniendo una traducción

francesa estándar, 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 traducción. En este caso, asumiremos que los archivos de traducción están localizados en el

mismo directorio que el ejecutable.

Las librerías 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 también, además de los anteriores, pero estos

son contribuciones de los usuarios de Qt y no son soportados oficialmente. Los archivos de traducción de las

librerías 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 traducción por vez, así que usamos un

QTranslator separado de los demás, para la traducción 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 traducción.

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 aplicación debe ser revertido o

modificado, y esto se puede hacer llamando a

QApplication::setLayoutDirection(Qt::RightToLeft). Los archivos de traducción 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 más conveniente para nuestros usuarios si proveemos a nuestra aplicación con los archivos de

traducción integrados en el ejecutable, usando el sistema de recursos de Qt. Hacer esto, no solo reduciría el

numero de archivos distribuidos como parte de nuestro producto sino también evitaría el riesgo de pérdidas

de los archivos de traducción o de que estos sean eliminados accidentalmente.

Suponiendo que los archivos .qm están ubicados en el subdirectorio traducciones en el árbol de

códigos fuente, tendríamos 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 contendría la siguiente entrada:

RESOURCES += miApp.qrc

205 17. Internacionalización

Finalmente, en el main(), debemos especificar :/traducciones como el patch para los archivos de

traducción. Los dos puntos al principio indican que el patch hace referencia a un recurso en contraposición a

un archivo en el sistema de archivos.

Hasta este momento hemos cubierto todo lo que se requiere para hacer una aplicación disponible para

trabajar usando traducciones a otros lenguajes. Pero el lenguaje y el sistema de dirección de escritura no son

el único aspecto que varía entre países y culturas. Un programa internacionalizado debe tener en cuenta el

formato de fecha y hora local, formatos monetarios, formatos numéricos y el orden de comparación de

cadenas de texto. Qt incluye una clase llamada QLocale que provee de formatos localizados de fecha/hora

y numéricos. Para consultar cualquier otra información con respecto a la localidad, podemos usar las

funciones estándar 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 local-

dependiente. Esto es realmente útil para la clasificación de los ítems visibles al usuario.

La función 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 aplicación 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 navegación atrás y adelante, deben ser intercambiados cuando se esté tratando con un lenguaje de derecha

a izquierda. Podemos hacer esto, como se muestra a continuación:

if (QApplication::isRightToLeft()) {

accionAtras->setIcon(iconoAlante);

accionAlante->setIcon(iconoAtras);

} else {

accionAtras->setIcon(iconoAtras);

accionAlante->setIcon(iconoAlante);

}

Los iconos que contengan caracteres alfabéticos muy comúnmente necesitan ser traducidos. Por ejemplo, la

letra „I‟ en una barra de herramientas asociada con la opción de Itálica de un procesador de texto, debe ser

reemplazada por una „C‟ en el español (Cursivo) y por una „K‟ en el Danés, Holandés, Alemán, 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 múltiples 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>

206 17. Internacionalización

Si la localidad del usuario es es (Español), :/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 Dinámico del Lenguaje

Para la mayoría de las aplicaciones, detectar el lenguaje preferido por el usuario en el main() y cargar los

archivos .qm apropiados es un método totalmente satisfactorio para sus fines. Pero existen algunas

situaciones donde el usuario puede necesitar la capacidad de poder cambiar el lenguaje de la aplicación

dinámicamente. Una aplicación que es usada continuamente por diferentes personas, puede necesitar que se

pueda cambiar el lenguaje sin tener que reiniciar la aplicación. Por ejemplo, las aplicaciones usadas por los

operadores de los “Centros de llamadas”, las aplicaciones usadas por varios traductores 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 aplicación sin reiniciar.

Hacer una aplicación capaz de cambiar el lenguaje dinámicamente requiere un poco más de trabajo que

cuando se lee un único archivo de traducción al inicio de la aplicación, pero no es difícil. Lo que se debe

hacer es:

Proporcionar un método mediante el cual el usuario pueda cambiar el lenguaje.

Para cada widget o diálogo, colocar todas sus cadenas de texto traducibles en una función separada

(a menudo llamada retraducirUi()) y llamar a dicha función cuando el lenguaje sea cambiado.

Vamos a repasar las partes más relevantes del código fuente de una aplicación de un “Centro de llamadas”.

La aplicación provee un menú de lenguaje (mostrado en la Figura 17.1), para permitir al usuario establecer

el lenguaje en tiempo de ejecución. El lenguaje por defecto es el inglés.

Puesto que no sabemos cuál lenguaje quiere usar el usuario cuando se inicie la aplicación, ya no cargaremos

las traducciones en la función main(). En lugar de eso, las cargaremos dinámicamente cuando sean

necesitadas, de forma que todo el código al que necesitemos aplicar traducciones debe ir en la ventana

principal y en las clases de diálogos.

Echemos un vistazo a la subclase QMainWindow de la aplicación.

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 dinámico

207 17. Internacionalización

En el constructor, colocamos al widget central para que sea un objeto tipo JournalView, que es una

subclase de QTableWidget. Después, colocamos unas cuantas variables miembros relacionadas a la

traducción:

La variable Traductorapp es un objeto QTranslator usado para guardar las traducciones

actuales de la aplicación.

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 traducción de la aplicación.

Lo que hacemos es instalar dos objetos QTranslators en el QApplication: el objeto

Traductorapp guarda la traducción actual de la aplicación, 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() (también es una función 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 función 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 función 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 función crearMenus() crea los menús, pero no les da ningún título. Nuevamente, esto se hará en la

función retraducirUi().

A mitad de función, llamamos a la función crearMenuLenguaje() para llenar el menú Lenguaje con la

lista de los lenguajes soportados. Estudiaremos su código fuente en un momento. Primero veamos el código

de la función retraducirUi():

void MainWindow::retraducirUi()

{

accionNuevo->setText(tr("&Nuevo"));

accionNuevo->setShortcut(tr(“Ctrl+N”));

208 17. Internacionalización

accionNuevo->setStatusTip(tr("Crear una publicación 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 función 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 aplicación 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 estándar. Establecimos también el titulo de cada QMenu, así como también el titulo de la ventana.

La función crearMenus() presentada previamente llamó a la función crearMenuLenguaje() para

llenar el menú Lenguaje con una lista de lenguajes:

Vista del código:

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",

"Español");

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 == "Español")

accion->setChecked(true);

}

}

209 17. Internacionalización

En lugar de codificar internamente los lenguajes soportados por la aplicación, creamos una entrada de menú

por cada archivo .qm localizado en el directorio traducciones de la aplicación. Para mayor sencillez,

suponemos que el lenguaje Español posee también un archivo .qm. Una alternativa podría haber sido llamar

a la función clear() en los objetos QTranslator cuando el usuario eligiera Español 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, luciría muy rudimentario y confundiría a algunos usuarios. La solución usada en

crearMenuLenguaje() es revisar la traducción de la cadena de texto “Español” en el contexto

“MainWindow”. La cadena de texto debe ser traducida a “Deutsch” en una traducción alemana, a “Français”

en una traducción francesa, y a “ ” en una traducción japonesa.

Hemos creado un QAction chequeable para cada lenguaje y aloja el nombre de la localidad en el ítem

“data” de la acción. Los añadimos a un objeto QActionGroup para asegurar que solo un ítem del menú

Lenguaje es chequeado por vez. Cuando el usuario elija una acción del grupo, el QActionGroup emite la

señal triggered(QAction *), que está conectada a la función 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 traducción pertinentes para la aplicación 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 sección

Configuración 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 aplicación está siendo ejecutada, intentaremos cargar los

archivos de traducción correctos para la nueva localidad y llamamos a retraducirUi() para actualizar la

interfaz de usuario. En todos los casos, pasamos el evento la función de la clase base changeEvent(), ya

que a la clase base puede serle de importancia el LocaleChange o algún otro evento.

Hemos terminado nuestra revisión del código del MainWindow. Ahora veremos el código para una clase de

uno de los widget de la aplicación, la clase JournalView, para observar qué cambios son necesarios

hacerle para que soporte traducciones dinámicas.

210 17. Internacionalización

JournalView::JournalView(QWidget *parent)

: QTableWidget(parent)

{

...

retraducirUi();

}

La clase JournalView es una subclase de QTableWidget. Al final del constructor, llamamos a la

función 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 función 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 aplicación, esto sucede

cuando llamamos a la función 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 aplicación: “Quizá debas cargar una nueva

traducción”. Los eventos LanguageChange, por otro lado, son generados por el mismo Qt, y le dicen a los

widgets de la aplicación: “Tal vez deberían 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("Número de”

“Teléfono")<< tr("Asunto");

setHorizontalHeaderLabels(etiquetas);

}

La función retraducirUi() actualiza la columnas de cabeceras con nuevos textos traducidos,

completando el código relacionado a una traducción de un widget escrito a mano. Para los widgets y

diálogos desarrollados con Qt Designer, la herramienta uic genera automáticamente una función similar a

nuestra función retraducirUi() que es automáticamente 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

básicos:

1. Ejecutar lupdate para extraer todas las cadenas de texto visibles al usuario del código fuente de la

aplicación.

2. Traducir la aplicación usando Qt Linguist

211 17. Internacionalización

3. Ejecutar lrelease para generar los archivos binarios .qm que la aplicación puede cargar usando

QTranslator.

Los pasos 1 y 3 son realizados por los desarrolladores de la aplicación. El paso 2 es manejado por los

traductores. Este ciclo puede repetirse cada vez que sea necesario durante el desarrollo de la aplicación y en

toda su vida útil.

Como ejemplo, mostraremos cómo traducir la aplicación de hoja de cálculo hecha en el Capitulo 3. La

aplicación 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 aplicación para especificar a cuáles lenguajes

queremos dar soporte. Por ejemplo, si queremos dar soporte al idioma alemán y al francés, además del

español, añadiríamos al archivo spreadsheet.pro la siguiente entrada:

TRANSLATIONS = spreadsheet_de.ts \

spreadsheet_fr.ts

Aquí, especificamos que se usarán dos archivos de traducción: uno para el alemán y otro para el francés.

Estos archivos serán creados la primera vez que ejecutamos lupdate y es actualizado cada vez que

ejecutemos lupdate.

Estos archivos poseen normalmente una extensión .ts. Estos están 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 más

eficiencia para la máquina. Curiosamente, la extensión .ts hace alusión a “translation source” (que en

español sería “fuente de traducción” o “recurso de traducción”) y la extensión .qm hace alusión a un archivo

“Qt message” (cuya equivalente al español seria “mensaje de Qt”).

Suponiendo que estamos ubicados en el directorio que contiene el código fuente de la aplicación

Spreadsheet, podemos ejecutar lupdate sobre el archivo spreadsheet.pro desde la línea de

comandos, como se muestra a continuación:

lupdate -verbose spreadsheet.pro

La opción –verbose le dice a lupdate que proporcione más retroacción 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 código fuente de la aplicación es

guardada en los archivos .ts, junto con una traducción vacía. Las cadenas de texto que aparezcan en los

archivos .ui de la aplicación son incluidas también.

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 añadir una entrada CODECFORTR al archivo .pro. Por

ejemplo:

CODECFORTR = EUC-JP

Esto debe hacerse adicionalmente a la llamada a QTextCodec::setCodecForTr() desde la función

main() de la aplicación.

Las traducciones, luego, necesitan ser agregadas a los archivos spreadsheet_de.ts y

spreadsheet_fr.ts, usando Qt Linguist.

Para ejecutar Qt Linguist en Windows, diríjase a Qt by Trolltech v4.x.y/Linguist. Para ejecutarlo en

distribuciones Linux, escriba linguist en la línea de comandos. En Mac puede usar el Mac Os X Finder y

212 17. Internacionalización

hacer doble clic a Linguist. Para empezar a añadir 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 aplicación a ser traducidos. Para la aplicación 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 traducción y un

indicador Done que indica si está hecha la traducción o no. El aérea central derecha es donde podemos

ingresar una traducción para el ítem actual. El área inferior derecha, es un cuadro llamado Warnings donde

se muestran las advertencias y recomendaciones proporcionadas automáticamente 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.

Típicamente, empezaremos por traducir solo unas cuantas cadenas de texto y ejecutamos la aplicación 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 continuación:

lrelease -verbose spreadsheet.pro

Suponiendo que hemos traducido 19 cadenas de texto al francés 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 serán mostradas en su lenguaje original cuando ejecutemos la aplicación.

El flag Done es ignorado por lrelease; este puede ser usado por los traductores para identificar cuales

traducciones están finalizadas y cuáles deben ser revisadas.

Figura 17.2 Qt Linguist en acción

213 17. Internacionalización

Cuando modificamos el código fuente de la aplicación, los archivos de traducción pueden quedar obsoletos o

desactualizados. La solución 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 aplicación este casi lista para su lanzamiento.

Las herramientas lupdate y Qt Linguist son muy inteligentes. Las traducciones que no serán más 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 asociación que puede ahorrar un tiempo

considerable a los traductores con todos aquellos textos que son iguales o similares pero en diferentes

contextos.

Para más información acerca de Qt Linguist, lupdate y lrelease, diríjase al manual de Qt Linguist en

la siguiente dirección web: http://doc.trolltech.com/4.1/linguist-manual.html. El manual contiene una

explicación completa de la interfaz de usuario de Qt Linguist y tutoriales paso a paso para programadores.

214 18. Multithreading

18. Multithreading

Creando Threads (hilos)

Sincronizando Threads

Comunicándose con el Thread Principal

Usando Clases Qt en Threads Secundarios

Las aplicaciones GUI convencionales poseen un hilo (conocido como thread en inglés) de ejecución y

realizan una operación a la vez. Si el usuario invoca una operación que toma mucho tiempo desde la interfaz

de usuario, la interfaz típicamente se congelará mientras la operación esté en progreso. El Capitulo 7

(Procesamiento de Eventos) presenta soluciones a este problema. El multithreading es otra solución.

En una aplicación multi hilo o multi tarea (multithreading), la interfaz gráfica se ejecuta o corre en su propio

hilo y el procesamiento es llevado a cabo en uno más 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 simultáneamente en

diferentes procesadores, obteniendo así mejor rendimiento.

En este capítulo, empezaremos con mostrar cómo hacer subclases de QThread y cómo usar QMutex,

QSemaphore, y QWaitCondition para sincronizar varios hilos. Luego veremos cómo comunicarnos

con el hilo principal desde hilos secundarios mientras el ciclo de evento se está ejecutando. Finalmente,

terminaremos haciendo una revisión de las clases de Qt que pueden ser usadas en hilos secundarios y cuáles

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 programación multithreading, de manera que nos

enfocaremos más en explicar cómo desarrollar aplicaciones Qt con multithreading que en el tema mismo de

los hilos.

Creando Threads

Proporcionar múltiples hilos en una aplicación Qt es fácil de hacer: simplemente hacemos una subclase de

QThread y re implementamos la función run(). Para mostrar cómo funciona, comenzaremos revisando el

código para una subclase muy pequeña 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();

215 18. Multithreading

private:

QString mensajeStr;

volatile bool detenido;

};

La clase Hilo hereda de QThread y re implementa la función run(). También 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 leída lo más 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 función run() es llamada para empezar a ejecutar el hilo. Mientras la variable detenido sea false,

la función sigue imprimiendo el mensaje que se le ha pasado en la consola. El hilo termina cuando control

deja la función run().

void Hilo::detener() {

detenido = true;

}

La función 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 función puede ser llamada desde cualquier

hilo en cualquier momento. Para los propósitos de este ejemplo, asumimos que la asignación de un bool es

una operación atómica. Esta es una suposición lógica, considerando que un bool solamente puede tener dos

estados. Más adelante en esta sección veremos cómo usar QMutex para garantizar que la asignación a una

variable es una operación atómica.

QTread provee una función llamada terminate() que termina la ejecución del hilo mientras éste está

ejecutándose. Usar terminate() no es recomendable, ya que ésta puede detener el hilo en cualquier

punto y no le da al hilo ningún chance de limpiarse después. Siempre es más seguro usar una variable

detenido y una función detener() como lo hicimos aquí.

Figura 18.1 La aplicación Hilos

Ahora veremos cómo usar la clase Hilo en una pequeña aplicación que use dos hilos, A y B,

adicionalmente al hilo principal.

216 18. Multithreading

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 básica.

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 botón para el hilo A, la función iniciarODetenerHiloA() detiene

el hilo si éste estaba ejecutándose, sino estaba ejecutándose entonces lo inicia. Esta función también actualiza

el texto del botón.

void DialogoHilo::iniciarODetenerHiloB()

{

if (hiloB.isRunning()) {

hiloB.detener();

botonHiloB->setText(tr("Iniciar B"));

} else {

217 18. Multithreading

hiloB.start();

botonHiloB->setText(tr("Detener B"));

}

}

El código 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 aplicación se cierre de una

manera limpia, aunque esto no importe tanto para este ejemplo.

Si ejecutas la aplicación 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 común para las aplicaciones multi hilos es que éstas sincronicen varios de ellos. Qt proporciona

las siguientes clases de sincronización: QMutex, QReadWriteLock, QSemaPhore y

QWaitCondition.

La clase QMutex provee una manera de proteger una variable o una pieza de código que solamente pueda

ser accedida vez por vez (véase MUTEX en el Glosario de terminos). La clase proporciona igualmente una

función 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 también proporciona una función tryLock()

que retorna inmediatamente si el mutex ya está bloqueado.

Por ejemplo, supongamos que queríamos proteger con un QMutex la variable detenido de la clase Hilo

de la sección anterior. Entonces lo que haríamos seria añadir el siguiente dato miembro a la clase Hilo:

private: •••

QMutex mutex;

};

La función run() se cambiaría por esta:

void Hilo::run()

{

porsiempre {

mutex.lock();

if (detenido) {

detenido = false;

mutex.unlock();

break;

}

218 18. Multithreading

mutex.unlock();

cerr << qPrintable(mensajeStr);

}

cerr << endl;

}

La función detener() se transformaría 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, podríamos reescribir las funciones

previas run() y detener() como sigue a continuación:

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 simultáneamente (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 sincronización que permite

accesos simultáneos de sólo lectura sin comprometer el rendimiento.

En la clase Hilo, no tendría 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 más apropiado incluiría uno o más hilos lectores accediendo a algún dato compartido y

uno o varios hilos escritores modificando ese dato. Por ejemplo:

MiDato dato;

QReadWriteLock bloquear;

void HiloLector::run()

{

...

bloquear.lockForRead();

219 18. Multithreading

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 generalización de los mutex, pero a distinción de los bloqueadores de

lectura/escritura, los semáforos (tipo de dato QSemaphore) pueden ser usados para salvaguardar cierto

número de recursos idénticos. Los siguientes dos segmentos de código muestran la correspondencia entre

QSemaphore y QMutex:

QSemaphore semaforo(1); QMutex mutex;

semaforo.acquire(); mutex.lock();

semaforo.release(); mutex.unlock();

Pasando el numero 1 al constructor; le decimos al semáforo que controla un único recurso. La ventaja de usar

un semáforo es que podemos pasar otros números distintos a 1 al constructor y luego llamar a la función

acquire() múltiples veces para adquirir muchos recursos.

Una aplicación típica de los semáforos se hace cuando se transfiere cierta cantidad de datos

(TamañoDeDatos) entre dos hilos usando un buffer circular de un cierto tamaño (TamañoDeBuffer):

const int TamañoDeDatos = 100000;

const int TamañoDeBuffer = 4096;

char buffer[TamañoDeBuffer];

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 pequeño 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 rápido, este sobre escribirá datos que el consumidor no haya leído todavía; si el consumidor lee

los datos muy rápido, 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 leído el buffer completo, y así sucesivamente. Sin embargo, en maquinas con varios

procesadores, esto no es tan rápido 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 semáforos:

220 18. Multithreading

QSemaphore espacioLibre(TamañoDeBuffer);

QSemaphore espacioUsado(0);

El semáforo espacioLibre maneja la parte del buffer que el productor puede llenar con datos. El

semáforo espacioUsado maneja el área que el consumidor puede leer. Estas dos áreas son

complementarias. El semáforo espacioLibre es inicializado con el TamañoDeBuffer en 4096,

significando eso que éste tiene esos grandes recursos que pueden ser adquiridos.

Cuando la aplicación comience, el hilo lector empezará adquiriendo bytes “libres” y convirtiéndolos a bytes

“usados”. El semáforo 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 aplicación del mundo real, tendríamos que

operar probablemente en largas unidades (por ejemplo, 64 o 256 bytes por vez) para reducir la sobrecarga

asociada con el uso de semáforos.

void Productor::run()

{

for (int i = 0; i < TamañoDeDatos; ++i) {

espacioLibre.acquire();

buffer[i % TamañoDeBuffer]= "ACGT"[uint(rand()) % 4];

espacioUsado.release();

}

}

En el productor, cada iteración empieza adquiriendo un byte “libre”. Si el buffer está lleno de datos que el

consumidor no haya leído, 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 algún dato aleatorio („A‟, „C‟,

„G‟ o „T‟) y liberaremos el byte como “usado”, para que pueda ser leído por el hilo consumidor.

void Consumidor::run()

{

for (int i = 0; i < TamañoDeDatos; ++i) {

espacioUsado.acquire();

cerr << buffer[i % TamañoDeBuffer];

espacioLibre.release();

}

cerr << endl;

}

En el consumidor, empezamos adquiriendo un byte “usado”. Si el buffer no contiene ningún dato para leer,

el llamado a acquire() se bloqueará hasta que el productor haya producido algún dato. Una vez que

hayamos adquirido el byte, lo imprimimos y liberamos el byte como “libre”, haciéndole 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 después es que el

productor convierte algún espacio “libre” en espacio “usado”, y el consumidor luego puede convertirlo a

espacio “libre” nuevamente.

221 18. Multithreading

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 impresión 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 fáciles de entender como sea posible,

podemos usar valores pequeños para los buffers TamañoDeDatos y TamañoDeBuffer.

Por ejemplo, aquí está una posible corrida con un TamañoDeDatos de 10 y un TamañoDeBuffer 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 semáforos dan un montón de alcances al programador del sistema de hilos,

el cual puede estudiar el comportamiento de los hilos y elegir una política de programación adecuada.

Un método 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 condición se

haya cumplido. Esto da un margen de mayor precisión en el control del que es posible con el método de usar

solamente varios mutex. Para mostrar cómo funciona, vamos a rehacer el ejemplo de productor-consumidor

usando condiciones de espera (wait conditions).

const int TamañoDeDatos = 100000;

const int TamañoDeBuffer = 4096;

char buffer[TamañoDeBuffer];

QWaitCondition bufferNoEstaLLeno;

QWaitCondition bufferNoEstaVacio;

QMutex mutex;

int espacioUsado = 0;

Además del buffer, declaramos dos QWaitConditions, un QMutex, y una variable que alojará cuántos

bytes, en el buffer, son bytes “usados”.

void Productor::run()

{

for (int i = 0; i < TamañoDeDatos; ++i) {

mutex.lock();

while (espacioUsado == TamañoDeBuffer)

bufferNoEstaLLeno.wait(&mutex);

buffer[i % TamañoDeBuffer]= "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

condición “buffer no está lleno”. Cuando esa condición se cumpla, escribimos un byte al buffer,

incrementamos la variable espacioUsado, y activamos cualquier hilo que espere que la condición de

“buffer no está vacío” sea activada.

Usamos un mutex para proteger todos los accesos a la variable espacioUsado. La función

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 == TamañoDeBuffer)

bufferNoEstaLleno.wait(&mutex);

Por esta otra operación:

if (espacioUsado == TamañoDeBuffer) {

222 18. Multithreading

mutex.unlock();

bufferNoEstaLleno.wait();

mutex.lock();

}

Sin embargo, esto se rompería tan pronto como permitamos más de un hilo productor, ya que otro productor

podría tomar al mutex inmediatamente después del llamado a wait() y hacer que la condición “buffer no

está lleno” sea falsa nuevamente.

void Consumidor::run()

{

for (int i = 0; i < TamañoDeDatos; ++i) {

mutex.lock();

while (espacioUsado == 0)

bufferNoEstaVacio.wait(&mutex);

cerr << buffer[i % TamañoDeBuffer];

--espacioUsado;

bufferNoEstaLleno.wakeAll();

mutex.unlock();

}

cerr << endl;

}

El consumidor hace exactamente lo opuesto que el productor: espera a que la condición “buffer no está

lleno” se cumpla y activa cualquier hilo en espera de la condición “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 método muy adecuado es usar la clase QThreadStorage

<T>.

Un uso común para QThreadStorage<T> es para las cachés. 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 función 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.

223 18. Multithreading

Comunicándose con el Thread Principal

Cuando una aplicación Qt se ejecuta, solamente un hilo empieza a correr – el hilo principal. Este es el único

hilo al que le es permitido la creación del objeto QCoreApplication y de llamar a exec() con éste.

Después del llamado a exec(), éste hilo también espera por un evento o por el procesamiento de un evento.

El hilo principal puede iniciar nuevos hilos a través de la creación de objetos de una subclase de QObject,

como lo hicimos en la sección anterior. Si esos nuevos hilos necesitan comunicarse entre ellos, pueden usar

variables compartidas junto con los mutex, bloqueadores de lectura/escritura, semáforos o condiciones de

espera. Pero ninguna de estas técnicas puede usarse para comunicarse con el hilo principal, puesto que se

podría bloquear el ciclo de evento y congelar la interfaz de usuario.

La solución 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 están conectados a una señal son invocados inmediatamente cuando la señal es

emitida, usando un llamado directo a una función.

Sin embargo, cuando conectamos objetos que “habitan” en diferentes hilos, el mecanismo se vuelve

asíncrono (Este comportamiento puede cambiarse a través de un quito parámetro opcional a

QObject::connect()). Detrás 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 aplicación Image Pro

Para ilustrar cómo funcionan las conexiones de signals y slots entre hilos, vamos a revisar el código de la

aplicación Image Pro, una aplicación de procesamiento de imagen muy básica que le permite al usuario rotar,

redimensionar o cambiar la profundidad de color de una imagen. La aplicación usa un hilo secundario para

realizar operaciones sobre imágenes sin bloquear el ciclo de evento. Esto hace una diferencia significante

cuando se procesan imágenes 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);

224 18. Multithreading

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 señales emitidas por el objeto HiloTransaccion, el cual revisaremos en un momento.

void VentanaImagen::voltearHorizontalmente()

{

agregarTransaccion(new TransaccionVoltear(Qt::Horizontal));

}

El slot voltearHorizontalemente() crea una transacción de “rotacion” y la registra usando la

función 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 función agregarTransaccion() agrega una transacción a la línea de transacciones del hilo

secundario y deshabilita las acciones Abrir, Guardar y Guardar Como mientras las transacciones están 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 línea de transacciones de

TransacctionThread queda vacía.

Ahora, vamos a ver la clase HiloTransaccion:

class HiloTransaccion : public QThread

{

225 18. Multithreading

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

después de la otra en el segundo plano.

void HiloTransaccion::agregarTransaccion(Transaccion *transac)

{

QMutexLocker locker(&mutex);

transacciones.enqueue(transac);

if (!isRunning())

start();

}

La función agregarTransaccion() agrega una transacción a la línea de transacciones e inicia el hilo

de transacción 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 través 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();

226 18. Multithreading

emit transaccionIniciada(transac->mensaje());

QImage nuevaImagen=transac->aplicar(imagenAnterior);

delete transac;

mutex.lock();

imagenActual = nuevaImagen;

mutex.unlock();

}

}

La función run() revisa a través de la línea de transacción y ejecuta cada transacción en turno mediante la

llamada al método aplicar() de cada transacción.

Cuando una transacción es iniciada, emitimos la señal transaccionIniciada() con un mensaje para

mostrarlo en la barra de estatus de la aplicación. Cuando todas las transacciones se hayan procesado, la

función run() retorna y QThread emite la señal 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 través de un puntero Transaccion (De todas formas, si omitimos este paso, alguno

compiladores emitirán 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 parámetro que especifica la orientación de la rotación

(horizontal o vertical).

QImage TransaccionVoltear::aplicar(const QImage &imagen)

{

return imagen.mirrored(orientacion == Qt::Horizontal,

orientacion == Qt::Vertical);

}

La función aplicar() llama a la función QImage::mirrored() sobre el objeto QImage que recibe

como parámetro y retorna el QImage resultante.

QString TransaccionVoltear::mensaje()

{

if (orientacion == Qt::Horizontal) {

return QObject::tr("Volteando imagen

horizontalmente...");

} else {

227 18. Multithreading

return QObject::tr("Volteando imagen

verticalmente...");

}

}

La función mensaje() retorna el mensaje a mostrar en la barra de estado mientras la operación está en

progreso. Esta función es llamada en HiloTransaccion::run() cuando se emite la señal

transaccionIniciada().

Usando Clases Qt en Threads Secundarios

Se dice que una función es thread-safe (segura en el uso de hilos) cuando esta puede ser llamada desde

diferentes hilos simultáneamente de una manera segura. Si dos hilos seguros son llamados desde hilos

diferentes sobre los mismos datos compartidos, el resultado está siempre definido. Por extensión, puede

decirse que una clase es thread-safe cuando todas sus funciones pueden ser llamadas desde diferentes hilos

simultáneamente sin interferir con los demás, 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 mayoría de las clases de Qt que no son de interfaz grafica cumplen un requerimiento menos estricto: Son

reentrant (que permite muchas entradas [puede ser leída simultáneamente por varios usuarios o

varias veces por el mismo usuario]). Una clase es reentrant si diferentes instancias de la clase pueden ser

usadas simultáneamente en diferentes hilos. No obstante, acceder al mismo objeto reentrant en múltiples

hilos simultáneamente no es seguro, y dichos accesos deberían ser protegidos con un mutex. Típicamente,

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 (también 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 función

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 librerías 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

228 18. Multithreading

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 señal

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 implícito como una técnica de optimización. Mientras que ésta optimización usualmente

hace a una clase no reentrant, en Qt esto no un problema porque Qt usa instrucciones atómicas en lenguaje

ensamblador para implementar contadores referenciales y que sean thread-safe, haciendo que las clases

implícitamente compartidas de Qt sean reentrant.

El modulo SQL de Qt puede usarse también en aplicaciones multi hilos, pero este tiene sus propias

restricciones, que varían 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.

229 19. Creando Plugins

19. Creando Plugins

Extendiendo Qt con Plugins

Haciendo Aplicaciones que Acepten Plugins

Escribiendo Plugins para Aplicaciones

Las librerías dinámicas (también llamadas librerías compartidas o DLLs) son módulos independientes que

son guardados en un archivo separado en el disco y pueden ser utilizados por múltiples aplicaciones. Los

programas especifican, usualmente, las librerías dinámicas que necesitan en tiempo de enlazado, caso donde

las librerías son cargadas automáticamente cuando la aplicación comienza. Este método a menudo implica

tener que añadir la librería y posiblemente su directorio de include al archivo .pro de la aplicación 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 dinámicamente la librería cuando sea requerido, y luego determinar los símbolos 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 librería,

QLibrary busca la librería en las locaciones estándares 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 librería dinámica 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 imágenes, drivers de

bases de datos, estilos de widgets, codificaciones de texto, y accesibilidad. La primera sección de este

capítulo muestra cómo extender Qt con plugins de Qt.

También es posible crear plugins específicos para aplicaciones Qt particulares. Qt hace fácil la tarea de

escribir tales plugins a través de su framework para plugins, el cual agrega seguridad de colisión y

comodidad para trabajar con QLibrary. En las últimas dos secciones de este capítulo, mostramos cómo

hacer una aplicación que soporte plugins y cómo crear plugins personalizados para una aplicación.

Extendiendo Qt con Plugins

Qt puede ser extendido con una variedad de plugins de diferentes tipos, los más comunes son driver de bases

de datos, formatos de imagen, estilos y códecs de texto. Para cada tipo de plugin, normalmente necesitamos

al menos dos clases: una clase que sea contenedora del plugin que implemente las funciones genéricas de la

API del plugin, y una o más clases manejadoras que implementen la API para un tipo particular de plugin.

Los manejadores son accedidos a través de la clase contenedora. Estas clases se muestran en la Figura 19.1.

230 19. Creando Plugins

Figura 19.1 Plugin Qt y clases manejadoras

Para demostrar cómo extender Qt con plugins, vamos a implementar un plugin que puede leer archivos de

cursores monocromáticos de Windows (archivos .cur). Estos archivos pueden contener muchas imágenes

del mismo cursor en diferentes tamaños. 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 través 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 también podría mostrarse con aplicaciones Qt ya que estas automáticamente verifican las

locaciones estándar 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 función claves() retorna una lista de formatos de imagen que el plugin soporta. El parámetro

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 extensión 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 extensión.

QImageIOPlugin::Capabilities

CursorPlugin::capacidades(QIODevice *device,

const QByteArray &formato) const

{

if (formato == "cur")

return CanRead;

if (formato.isEmpty()) {

ManejadorCursor manejador;

231 19. Creando Plugins

manejador.setDevice(device);

if (manejador.canRead())

return CanRead;

}

return 0;

}

La función 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 implementación retorna CanRead. Si ningún 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 función 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 leído 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 función 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 más. Si varios archivos tienen que ser

leídos, 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 parámetro es un nombre arbitrario que le queramos dar al plugin. El segundo

parámetro 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 públicas. 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;

232 19. Creando Plugins

mutable int numImagenes;

};

Las signaturas de todas las funciones públicas están 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 número de la

imagen actual del cursor para el primer cursor, pero como establecemos numImagenes en 0, es claro que

no poseemos imágenes todavía.

bool ManejadorCursor::canRead() const

{

if (estado == CabeceraAnterior) {

return device()->peek(4) == QByteArray("\0\0\2\0", 4);

} else {

return estado != Error;

}

}

La función canRead() puede ser llamada en cualquier momento para determinar si el manejador de

imagen puede leer más datos desde el objeto (device). Si la función es llamada antes de que hayamos leído

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 función trivial retorna el número 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

públicas, 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 función LeerCabeceraSiEsNecesario() en

aquellas funciones que dependan de que la cabecera haya sido leída.

int ManejadorCursor::ContarImagenes() const

{

LeerCabeceraSiEsNecesario();

return numImagenes;

}

Esta función retorna el número de imágenes en el archivo. Para un archivo válido donde ningún error de

lectura haya ocurrido, ésta retornará un número mayor o igual a 1.

233 19. Creando Plugins

Figura 19.2 Formato de archivo .cur

La siguiente función es muy compleja, así que vamos a revisarla por partes:

bool ManejadorCursor::leer(QImage *imagen)

{

LeerCabeceraSiEsNecesario();

if (estado != ImagenAnterior)

return false;

La función leer() lee los datos para cualquier imagen que comience en la posición actual del puntero al

objeto (device). Si la cabecera del archivo es leída satisfactoriamente, o después de que una imagen haya

sido leída y el puntero al objeto (device) esté en el comienzo de otra imagen, podemos leer la siguiente

imagen.

quint32 tamaño;

quint32 ancho;

quint32 alto;

quint16 numPlanos;

quint16 bitsPorPixel;

quint32 compresion;

QDataStream in(device());

in.setByteOrder(QDataStream::LittleEndian);

in >> tamaño;

if (size != 40) {

entrarEstadoError();

return false;

}

in >> ancho >> alto >> numPlanos >> bitsPorPixel >> compresion;

alto /= 2;

if (numPlanos != 1 || bitsPorPixel != 1 ||

compresion != 0) {

entrarEstadoError();

return false;

}

in.skipRawData((tamaño - 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 versión para un QDataStream ya que el formato de números enteros y de punto flotante no

varían entre las versiones de data stream. A continuación, leemos en varios ítems de datos de cabecera del

234 19. Creando Plugins

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

monocromático. Si tenemos algún 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 máscara XOR y la otra una máscara AND.

Las leemos dentro de QBitArray y no en QBitmaps. ¿Por qué? Porque un QBitmap es una clase

diseñada 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 tamaño 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:

235 19. Creando Plugins

Los pixeles blancos, negros y transparentes no son un problema, pero no existe una manera de obtener un

pixel de fondo invertido usando la especificación 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 número actual de imágenes 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 función SaltarASiguienteImagen() es usada para saltar una imagen. Por simplicidad,

simplemente llamamos a leer() e ignoramos la QImage resultante. Una implementación más eficiente

usaría la información guardada en la cabecera del archivo .cur para saltar directamente a la sección

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 función privada LeerCabeceraSiEsNecesario() es llamada desde ContarImagenes() y

desde leer(). Si la cabecera del archivo ya ha sido leída, el estado no será CabeceraAnterior y

236 19. Creando Plugins

retornamos inmediatamente. De otra forma, abrimos un data stream en el objeto (device), leemos en

alguna data genérica (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 imágenes válidas y establecemos el estado a Error. Una vez en el

estado Error, El estado del manejador no puede cambiar.

Vista del código:

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 función leerBitmap() es usada para leer una máscara de cursor AND y una máscara de XOR. Estas

máscaras tienen dos características inusuales. Primero, estas guardan las filas desde abajo hacia arriba, en

lugar de usar el método más común 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 máscara un byte

a la vez, debemos ir alternando bits e ir usando máscaras para extraer sus valores correctos.

Esto completa la implementación del plugin de cursor de Windows. Los plugins para otro tipo de formatos

de imagen deberían seguir el mismo modelo, aunque puede que se tengan que implementar más contenido de

la API de QImageIOHandler, en particular, las funciones usadas para la escritura de imágenes. Los

plugins de otro tipo, por ejemplo, códecs de texto o drivers de base de datos, siguen el mismo patrón 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:

237 19. Creando Plugins

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 librería, no una aplicación stand-alone. La línea CONFIG es usada para decirle a Qt que la

librería no es sólo una librería plana, sino una librería plugin. La línea 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 están instaladas, es mejor especificar cuál de ellas usar en el archivo .pro – por ejemplo, agregando la

línea

CONFIG += release

Las aplicaciones que usen plugins de Qt deben ser mostradas con los plugins que están intentando usar. Los

plugins de Qt deben estar ubicados en subdirectorios específicos (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 aplicación reside, de modo que para plugins de imágenes ellas buscan el directorio

aplicación_dir/plugins/imageformats. Si queremos mostrar plugins Qt en un directorio

diferente, el path de búsqueda de plugins puede ser aumentado usando

QCoreApplication::addLibraryPath().

Haciendo Aplicaciones que Acepten Plugins

Un plugin de aplicación es una librería dinámica que implementa una o más interfaces. Una interfaz es una

clase que consiste exclusivamente de funciones virtuales. La comunicación entre la aplicación y los plugins

es hecha a través de la tabla virtual de la interfaz. En esta sección, nos centraremos en cómo usar un plugin

en una aplicación Qt a través de sus interfaces, y en la siguiente sección mostraremos cómo implementar un

plugin.

Para proporcionar un ejemplo concreto, crearemos la aplicación sencilla llamada Text Art mostrada en la

Figura 19.3. Los efectos de texto son proporcionados por plugins; la aplicación retorna la lista de efectos de

texto provista por cada plugin e itera sobre ellas para mostrarlas como un ítem en un QListWidget.

La aplicación 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 &tamaño,

const QPen &pluma,

const QBrush &pincel) = 0;

};

Q_DECLARE_INTERFACE(InterfazTextArt, "com.software-

inc.TextArt.InterfazTextArt/1.0")

238 19. Creando Plugins

Figura 19.3 La aplicación Text Art

Una clase de interfaz normalmente declara un destructor virtual, una función virtual que retorna un

QStringList, y una o más 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 función 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 implementación de múltiples 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 aplicación, el nombre de la interfaz, y un número de versión. En

cualquier momento que modifiquemos la interfaz (p. e., si agregamos una función virtual o cambiamos la

signatura de una función existente), debemos recordar que tenemos que incrementar el número de versión; de

otra forma, la aplicación puede colapsar tratando de acceder a un plugin desactualizado.

La aplicación es implementada en una clase llamada DialogoTextArt. Mostraremos solamente el código

que es relevante para hacer una aplicación 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 función privada

cargarPlugins() para encontrar y cargar cualquier plugin que implemente la clase

InterfazTextArt y llena el list widget respectivamente llamando a otra función privada,

llenarListWidget().

void DialogoTextArt::cargarPlugins()

{

QDir pluginDir(QApplication::applicationDirPath());

239 19. Creando Plugins

#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 aplicación.

La función directoryOf(). (En Windows, el ejecutable de la aplicación 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 versión de Qt que la

aplicación, 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 querrán cargar una o más interfaces distintas. En estos casos, el código para obtener las

interfaces luciría 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 más de un puntero de interfaz, ya que es posible para los plugins

proporcionar múltiples interfaces usando herencia múltiple.

Vista del Código:

void DialogoTextArt::llenarListWidget(const QString &texto)

{

QFont fuente("Helvetica", iconSize.height(), QFont::Bold);

QSize tamIcono = listWidget->iconSize();

QPen pluma(QColor("darkseagreen"));

240 19. Creando Plugins

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 función llenarListWidget() comienza con la creación de algunas variables para pasarlas a la

función 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 sección, hemos visto cómo cargar plugins llamando a cargarPlugins() en el constructor, y

cómo hacer uso de ellos en llenarListWidget(). El código sale airoso si no hay plugins

proporcionando objetos tipo InterfazTextArt, si se proporciona uno, o más de uno. Además, los

plugins adicionales podrían ser agregados luego: Cada vez que la aplicación comience, debe cargar cuantos

plugin encuentre que proporcionen las interfaces que la aplicación quiera. Esto facilita la extensión de la

funcionalidad de la aplicación sin cambiar la aplicación en sí misma.

Escribiendo Plugins para Aplicaciones

Un plugin de aplicación es una subclase de QObject y de las interfaces que quiera proporcionar. Los

ejemplos que acompañan este libro incluyen dos plugins para la aplicación Text Art presentada en la sección

anterior, para mostrar que la aplicación maneja correctamente múltiples plugins.

Aquí, revisaremos el código para uno solo de ellos solamente, El plugin de Efectos Básicos. Supondremos

que el código fuente del plugin está localizado en un directorio llamado pluginefectosbasicos y que

la aplicación Text Art está ubicada en un directorio paralelo llamado textart. Aquí está la declaración 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 &tamaño,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 están subclaseadas para

asegurar la cooperación sin dificultades entre moc y qobject_cast<T>().

241 19. Creando Plugins

QStringList PluginEfectosBasicos::efectos() const

{

return QStringList() << "Plano" << "Contorno" << "Sombra";

}

La función 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 función 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 &tamaño, const

QPen &pluma, const QBrush &pincel)

{

QFont miFuente = fuente;

QFontMetrics medidas(miFuente);

while ((medidas.width(texto) > tamaño.width()

|| medidas.height() > tamaño.height())

&& miFuente.pointSize() > 9) {

miFuente.setPointSize(miFuente.pointSize() - 1);

medidas = QFontMetrics(miFuente);

}

Queremos asegurarnos de que el texto dado encajará en el tamaño especificado, de ser posible. Por esta

razón, 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 tamaño de la fuente hasta encontrar un tamaño que encaje, o hasta que

lleguemos a los 9 puntos, nuestro tamaño mínimo fijo.

QPixmap pixmap(tamaño);

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 tamaño requerido y un pintor (QPainter) para pintar sobre el pixmap. También

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(((tamaño.width() –

medidas.width(texto)) / 2) + 3,

(tamaño.height() -medidas.descent()) +

3, miFuente,texto);

242 19. Creando Plugins

pintor.drawPath(path);

pintor.setBrush(pincel);

}

Para el efecto “Plano”, ningún 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((tamaño.width() - medidas.width(texto)) / 2,

tamaño.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

capítulo:

TEMPLATE = lib

CONFIG += plugin

HEADERS = ../textart/interfaztextart.h \

pluginefectosbasicos.h

SOURCES = pluginefectosbasicos.cpp

DESTDIR = ../textart/plugins

Si este capítulo ha agudizado tu apetito por los plugins de aplicación, deberías estudiar los ejemplos más

avanzados de Plug & Paint provistos con Qt. La aplicación soporta tres interfaces diferentes e incluye un

dialogo muy útil de Información del Plugin que lista los plugins y las interfaces que están disponibles para la

aplicación.

243 20. Características Específicas de Plataformas

20. Características Específicas de Plataformas

Haciendo Interfaces con APIs Nativas

Usando Activex en Windows

Manejando la Administración de Sesión en X11

En este capítulo, haremos una revisión de las opciones específicas de plataformas (o de plataformas

especificas) disponibles para los programadores en Qt. Comenzamos viendo cómo 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 exploración de la extensión ActiveQt, mostrando cómo usar los controles ActiveX junto con

aplicaciones Qt/Windows y cómo crear aplicaciones que actúen como servidores ActiveX. En la última

sección, explicaremos cómo hacer que las aplicaciones Qt cooperen con el administrador de sesión en X11.

Adicionalmente a las características presentadas aquí, Trolltech ofrece muchas soluciones de plataformas

específicas, incluyendo los frameworks de migración Qt/Motif y Qt/MFC para facilitar la migración de

aplicaciones Motif/Xt y MFC a Qt. Una extensión similar para aplicaciones Tcl/Tk es proporcionada por

froglogic, y un convertidor de recursos Microsoft Windows está disponible desde Klarälvdalens Datakonsult.

Vaya a las siguientes páginas web para más 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 aplicación de plataforma Qtopia. Esta es estudiada en el

Capitulo 21.

Creando Interfaces con APIs Nativas

La API de Qt satisface la mayoría de las necesidades en todas las plataformas, pero en algunas

circunstancias, quizá queramos usar la API especifica de la plataforma. En esta sección, mostraremos cómo

usar las APIs nativas para las diferentes plataformas soportadas por Qt para realizar tareas particulares.

En cada plataforma, QWidget proporciona una función windId() que retorna el ID de la ventana o del

manejador. QWidget también provee una función estática 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

específicos de la plataforma. Por ejemplo, el siguiente código 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

244 20. Características Específicas de Plataformas

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

En X11, aquí está la manera cómo modificaríamos 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 código específico para la plataforma, asegura que la aplicación

seguirá compilando en otras plataformas.

Para una aplicación sólo para Windows, aquí hay un ejemplo de cómo 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 método QPaintDevice::paintEngine() para retornar

un puntero nulo y establecer el atributo Qt::WA_PaintOnScreen en el constructor del widget.

El próximo ejemplo muestra cómo 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();

245 20. Características Específicas de Plataformas

#endif

}

Combinando QPainter y llamados GDI de esta forma, algunas veces induce a resultados extraños,

especialmente cuando las llamadas a QPainter ocurren después de los llamados GDI, porque QPainter

hace algunas conjeturas o suposiciones acerca del estado de la capa básica de dibujo.

Qt define uno de las siguientes cuatro símbolos 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 también proporciona símbolos 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 también 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 ejecución, 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 específicas que

retornan los manejadores de bajo nivel al objeto básico. Estas están listadas en la Figura 20.2.

246 20. Características Específicas de Plataformas

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 gráfico X11 en un QPixmap o QWidget, por

ejemplo.

Las aplicaciones Qt que necesiten en su interfaz otros kits de herramientas o librerías 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 específicos de plataforma

más relevantes, uno de x11EventFilter(), winEventFilter(), macEventFilter() y

qwsEventFilter(). Alternativamente, podemos acceder a los eventos específicos de plataforma que son

enviados a un QWidget dado por medio de la re implementación 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 más información acerca de los asuntos específicos de plataformas, incluyendo cómo desarrollar

aplicaciones Qt en diferentes plataformas, vea http://doc.trolltech.com/4.1/winsystem.html.

247 20. Características Específicas de Plataformas

Usando Activex en Windows

La tecnología ActiveX de Microsoft permite a las aplicaciones incorporar componentes de interfaz de

usuario proporcionados por otras aplicaciones o librerías. 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 librerías que proveen componentes.

La Edición de Escritorio (Qt/Windows Desktop Edition) Qt/Windows proporciona el framework ActiveQt

para combinar limpiamente ActiveX y Qt. ActiveQt consta de dos módulos:

El módulo QAxContainer nos permite usar objetos COM e incrustar embebidamente controles

ActiveX en aplicaciones Qt.

El módulo 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 aplicación Qt usando el modulo QAxContainer. La aplicación Qt agrega un botón Open, un botón

Play/Pausa, un botón Stop y un slider al control ActiveX del Reproductor Windows Media.

Figura 20.3 La aplicación Media Player

La ventana principal de la aplicación 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);

248 20. Características Específicas de Plataformas

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 sección 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 creación 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

núcleo 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 métodos del control ActiveX están disponibles como propiedades Qt, signals y slots

mediante el objeto QAxWidget.

Figura 20.4 Árbol de herencia del módulo QAxContainer

Los tipos de datos COM son convertidos automáticamente a los tipos de datos Qt correspondientes, como se

resume en la Figura 20.5. Por ejemplo, un parámetro de entrada de tipo VARIANT_BOOL se transforma en

bool, y un parámetro de salida de tipo VARIANT_BOOL se convierte en bool &. Si el tipo resultante es

una clase Qt (QString, QDateTime, etc.), el parámetro 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 línea 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);

249 20. Características Específicas de Plataformas

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 Relación entre tipos COM y tipos Qt

Después 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 parámetro 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 patrón usual, excepto que conectamos algunas señales

Qt a slots proporcionados por el objeto COM (Play(), Pause() y Stop()). Como los botones son

similares, nosotros hemos mostrado aquí solamente la implementación del botón Stop.

Dejemos al constructor y veamos la función timerEvent():

250 20. Características Específicas de Plataformas

void PlayerWindow::timerEvent(QTimerEvent *event)

{

if (event->timerId() == updateTimer) {

double curPos=wmp->property("CurrentPosition").toDouble();

onPositionChange(-1, curPos);

} else {

QWidget::timerEvent(event);

}

}

La función 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 método

onPositionChange() para realizar la actualización.

No vamos a revisar el resto del código porque la mayoría 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 método COM

directamente (contrario a conectarlo a un signal de Qt). La manera más fácil de hacer esto es invocar

QAxBase::dynamicCall() con el nombre y la firma de el método como primer parámetro y los

argumentos al método como parámetros adicionales. Por ejemplo:

wmp->dynamicCall("TitlePlay(uint)", 6);

La función dynamicCall()ocupa hasta 8 parámetros 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 método 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 métodos que tengan tipos de datos no soportados en su lista de parámetros, podemos usar

QAxBase::queryInterface() para recuperar la interfaz COM y llamar el método directamente.

Como es costumbre con COM, debemos llamar a Release() cuando hayamos finalizado usando la

interfaz. Si necesitamos llamar muy a menudo tales métodos, 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,

señales o slots.

Ahora revisaremos el modulo QAxServer. Este modulo nos habilita para convertir un programa Qt estándar a

un servidor ActiveX. El servidor puede también ser una librería compartida o una aplicación stand-alone.

Los servidores construidos como librerías 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. También veremos cómo incrustar el widget en Internet Explorer.

Aquí está el comienzo de la definición de la clase del widget AxBouncer:

251 20. Características Específicas de Plataformas

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 están

implementadas por QAxServer.

Cuando hacemos herencia múltiple 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 sección

pública 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();

252 20. Características Específicas de Plataformas

El constructor de AxBouncer es un constructor estándar para un widget, con parámetro parent. El

macro QAXFACTORY_DEFAULT(), el cual usaremos para exportar el componente, espera un constructor

con esta firma.

La función 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 tendríamos si esto fuera un

widget Qt estándar.

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 función 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 deberían ser idealmente llamadas cuando sea que cambie

una propiedad. El método requestPropertyChange() le solicita permiso al cliente para cambiar una

propiedad, y retorna true si el cliente permite el cambio. La función propertyChanged() notifica al

cliente que la propiedad ha sido cambiada.

Las propiedades seteadoras setSpeed() y setRadius() también siguen este patrón, y de igual forma

lo hacen los slots start() y stop(), ya que estos cambian el valor de la propiedad running.

Aun queda una función miembro de AxBouncer muy interesante:

253 20. Características Específicas de Plataformas

QAxAggregated *AxBouncer::createAggregate()

{

return new ObjectSafetyImpl;

}

La función 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 estándar para deshacerse del infame mensaje de error de Internet Explorer:

“Object not safe for scrpting”.

Aquí está la definición 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 través de controllingUnknown(). Este

objeto COM es creado detrás de escena por el modulo QAxServer.

El macro QAXAGG_IUNKNOWN provee implementaciones estándares 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 función queryInterface() es una función 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

254 20. Características Específicas de Plataformas

| 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 sección mostrara como exportar

muchos controles ActiveX.

El primer argumento de QAXFACTORY_DEFAULT() es el nombre de la clase Qt a exportar. Este es

también 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 librería y el ID de la aplicación. Podemos usar

herramientas estándares como guidgen o uuidgen para generar estos identificadores. Como el server es

una librería, no necesitamos una función main().

Aquí está el archivo .pro para nuestro servidor ActiveX en-proceso:

TEMPLATE = lib

CONFIG += dll qaxserver

HEADERS = axbouncer.h \

objectsafetyimpl.h

SOURCES = axbouncer.cpp \

main.cpp \

objectsafetyimpl.cpp

RC_FILE = qaxserver.rc

DEF_FILE = qaxserver.def

Los archivos qaxserver.rc y qaxserver.def referido en el archivo .pro son archivos estándares

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 página HTML usando la etiqueta <object>:

255 20. Características Específicas de Plataformas

<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 aplicación de Libro de Direcciones (“Address Book”) escriptable. La

aplicación puede servir como una aplicación Qt/Windows estándar o un servidor ActiveX fuera-de-proceso.

La otra posibilidad nos permite escriptar la aplicación usando, por ejemplo, Visual Basic.

class AddressBook : public QMainWindow

{

Q_OBJECT

Q_PROPERTY(int count READ count)

Q_CLASSINFO("ClassID", "{588141ef-110d-4beb-95ab-

ee6a478b576d}")

Q_CLASSINFO("InterfaceID", "{718780ec-b30c-4d88-83b3-

79b3d9e78502}")

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 aplicación. La propiedad y los slots que este provee

estarán 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:

256 20. Características Específicas de Plataformas

Podemos hacer subclases de QAxFactory, re implementando sus funciones virtuales para

proporcionar información acerca de los tipos que queremos exportar, y usar el macro

QAXFACTORY_EXPORT() para registrar la fábrica.

Podemos usar los macros QAXFACTORY_BEGIN(), QAXFACTORY_END(), QAXCLASS() y

QAXTYPE() para declarar y registrar la fabrica. Este método requiere que especifiquemos los ID de

la clase y de la interfaz usando Q_CLASSINFO().

De vuelta a la definición 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,

señales y slots a los clientes, sino también aquellos de sus superclases hasta QWidget. El atributo

toSuperClass nos permite especificar la clase más alta (en el árbol de herencia) que queremos mostrar.

Aquí, especificamos el nombre de la clase del componente (AddressBook) como la superclase más alta a

exportar, significando esto que las propiedades, señales, y slots definidos en las superclases de

AddresBook no serán 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-96be-

461c2cd0d282}")

Q_CLASSINFO("InterfaceID", "{c8bc1656-870e-48a9-9937-

fbe1ceff8b2e}")

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 también 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 función main(), verificamos si la aplicación está siendo ejecutada como stand-alone o como un

servidor. La opción de línea de comando –activex es reconocida por QApplication y hace que la

257 20. Características Específicas de Plataformas

aplicación corra como un servidor. Si la aplicación no corre como un servidor, creamos el widget principal y

lo mostramos como lo haríamos normalmente en una aplicación stand-alone de Qt.

Adicionalmente a –activex, los servidores ActiveX entienden las siguientes opciones de línea 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 aplicación 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 fábrica 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 librería; el segundo argumento es el ID

de la aplicación. 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 = app

CONFIG += qaxserver

HEADERS = abitem.h \

addressbook.h \

editdialog.h

SOURCES = abitem.cpp \

addressbook.cpp \

editdialog.cpp \

main.cpp

FORMS = editdialog.ui

RC_FILE = qaxserver.rc

El archivo qaxserver.rc referido en el archivo .pro es un archivo estándar 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 revisión al framework ActiveQt. La distribución de Qt incluye ejemplos adicionales, y

la documentación contiene información acerca de cómo construir los módulos QAxContainer y

QAxServer y cómo resolver asuntos comunes de interoperabilidad.

Manejando la Administración de Sesión en X11

Cuando cerramos sesión en X11, algunos manejadores de ventanas nos preguntan si queremos guardar la

sesión. Si decimos que si, las aplicaciones que estaban ejecutándose son automáticamente reiniciadas la

siguiente vez que iniciemos sesión, con la misma posición de pantalla e, idealmente, con el mismo estado

que éstas tenían cuando cerramos sesión.

258 20. Características Específicas de Plataformas

El componente especifico de X11 que se encarga de guardar y restaurar la sesión es llamado el

administrador de sesión. Para hacer una aplicación Qt/X11 sensitiva al administrador de sesión, debemos re

implementar QApplication::saveState() y guardar el estado de la aplicación allí.

Windows 2000 y XP, y algunos sistemas Unix, ofrecen un mecanismo diferente llamado hibernación.

Cuando el usuario coloca la computadora en hibernación, 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 sesión en KDE

Cuando el usuario inicia un shutdown; osea, cuando inicia el proceso de apagar la máquina, podemos tomar

el control justo antes de que el apagado ocurra por medio de la re implementación de

QApplication::commitData(). Esto nos permite guardar cualquier data no guardada e interactuar

con el usuario si se requiere. Esta parte de la administración de sesión es soportada en X11 y Windows.

Exploraremos la administración de sesión hiendo a través de códigos de una aplicación llamada Tic-Tac-Toe

sensible a eventos de sesión. Primero, veamos la función 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 métodos commitData() y saveState() para soportar la administración de sesión.

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 sesión restaure los taaños de la ventana y la

posición.

259 20. Características Específicas de Plataformas

Figura 20.8 La aplicación Tic-Tac-Toe

Aquí está la definición 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 función saveState() es llamada cuando el administrado de sesión quiere que la aplicación

guarde su estado. La función está disponible en otras plataformas de todas maneras, pero nunca es llamada.

El parámetro QSessionManager nos permite comunicarnos con el administrador de sesión.

Comenzamos con pedirle al widget TicTacToe que guarde su estado en un archivo. Luego establecemos el

comando de exclusión del administrador de sesión (setdiscardCommand). Un comando de exclusión

(discard command) es un comando que el administrador de sesión debe ejecutar para eliminar cualquier

información 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 sesión, y rm es el

comando Unix estándar para remover archivos.

El administrador de sesión también posee un comando de reiniciar (restart command). Éste es el comando

que el administrador de sesión debe ejecutar para reiniciar la aplicación. Por defecto, Qt proporciona los

siguientes comandos de reiniciado:

260 20. Características Específicas de Plataformas

appname -session id_key

La primera parte, appname, es derivada de argv[0]. La parte id es el ID de sesión provisto por el

administrador de sesión; está garantizado que éste sea único entre las diferentes aplicaciones y entre las

diferentes instancias ejecutadas de la misma aplicación. La parte key es añadida únicamente para

identificar el momento en el cual el estado fue guardado. Por distintos motivos, el administrador de sesión

puede llamar a saveState() en múltiples ocasiones durante la misma sesión, 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 aplicación esté en la variable de entorno PATH si queremos que la aplicación 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, podríamos guardar el estado como un argumento de

comando de línea 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 exclusión para remover el

archivo.

void Application::commitData(QSessionManager &sessionManager)

{

if (ticTacToe->gameInProgress()

&& sessionManager.allowsInteraction()) {

int r = QMessageBox::warning(ticTacToe, tr("Tic-Tac-

Toe"),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 función commitData() es llamada cuando el usuario cierra su sesión. Podemos re implementarla para

que sea un mensaje emergente (pop up message) de alerta o advertencia al usuario acerca del la potencial

pérdida de datos. La implementación por defecto cierra todos los widget de ultimo nivel, los cuales resultan

en el mismo comportamiento como cuando el usuario cierra las ventanas una detrás de otra haciendo clic en

el botón de cerrar en sus pequeñas barras. En el Capitulo 3, vimos como re implementar el método

closeEvent() para captar cuando eso pase y mostrar un mensaje emergente (pop up).

Para los propósitos de este ejemplo, re implementamos el método commitData() y mostramos un

mensaje emergente preguntando al usuario que confirme si desea cerrar sesión si una partida está en progreso

y si el administrador de sesión nos permite interactuar con el usuario. Si el usuario hace clic en Yes,

lamamos a release() para decirle al administrador de sesión que continúe con el cierre de sesión; si el

usuario hace clic en No, llamamos a cancel() para cancelar el cierre de sesión.

261 20. Características Específicas de Plataformas

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 métodos sizeHint(), paintEvent(),

y mousePressEvent(). Este también 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 aplicación fue invocada con la opción –session, llamamos

a la función privada restoreState() para recargar la sesión antigua.

262 20. Características Específicas de Plataformas

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 función 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 vacías.

QString TicTacToe::sessionFileName() const

{

return QDir::homePath() + "/.tictactoe_" + qApp->sessionId()+

"_" + qApp->sessionKey();

}

La función privada sessionFileName() retorna el nombre de archivo para el ID de sesión actual y la

clave de sesión. Esta función es usada por saveState() y restoreState(). El nombre de archivo es

derivado del ID de sesión y de la clave de sesión.

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();

}

263 20. Características Específicas de Plataformas

En restoreState(), leemos el archivo que corresponde a la sesión restaurada y rellenamos el tablero

con esa información. Deducimos el valor de turnNumber a partir del número 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 aplicación fue guardado, y así

sessionFileName() retorna el nombre del archivo para esa sesión.

Probar y debuguear la administración de sesión puede llegar a ser frustrante, porque necesitamos loguearnos

y desloguearnos todo el tiempo. Una manera de evitar esto es usar la utilidad estándar xsm provista por X11.

La primera vez que invoquemos a xsm, este crea ventanas emergentes para un administrador de sesión y una

terminal. Las aplicaciones que iniciemos desde ese terminal usaran a xsm como su administrador de sesión

en lugar del usual, el administrador de sesión del sistema. Podemos usar la ventana de xsm para terminar,

reiniciar o excluir una sesión, y ver si nuestra aplicación se comporta como debería. Para más detalles acerca

de cómo hacer esto, vea http://doc.trolltech.com/4.1/session.html.

264 21. Programación Embebida

21. Programación Embebida

Iniciando con Qtopia

Personalizando Qtopia Core

Desarrollar software para ejecutarlo en dispositivos móviles tales como PDAs y teléfonos 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

más pequeñas que las computadoras de escritorio.

Qtopia Core (anteriormente llamado Qt/Embedded) es una versión de Qt optimizada para Linux embebido.

Qtopia Core proporciona la misma API y herramientas que la versión de escritorio de Qt (Qt/Windows,

Qt/X11 y Qt/Mac), y añade las clases y herramientas necesarias para la programación embebida. A través 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 distinción 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 más, Qtopia Core puede ser recompilado para excluir

características 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 estáticamente nuevamente a las

librerías de Qtopia Core.

Qtopia Core también se beneficia de varias características que son también parte de las versiones de

escritorio de Qt, incluyendo el uso extensivo del compartimiento de datos implícito (“copiar sobre escritura”;

en ingles: “copy on write”) como una técnica de ahorro de memoria, soporte para estilos de widgets

personalizados a través 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 programación embebida, lo cual también

incluye la Plataforma Qtopia, Qtopia PDA y Qtopia Phone. Estos proporcionan clases y aplicaciones

diseñadas específicamente para dispositivos portátiles 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 opción más común 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 versión de escritorio de Qt, tal como Qt/X11 o Qt/Windows, para la mayoría del desarrollo.

265 21. Programación Embebida

El sistema de configuración de Qtopia Core soporta compiladores cruzados, a través de configure y de la

opción de script –embedded. Por ejemplo para construir para una arquitectura ARM escribiríamos

./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 visualización

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 actúe como servidor. El servidor es responsable

de la asignación de regiones de pantalla a clientes para generar eventos de mouse y de teclado. Cualquier

aplicación Qtopia Core puede volverse un servidor especificando el comando –qws en su línea de comando

o pasando a QApplication::GuiServer como el tercer parámetro al constructor de QApplication.

Las aplicaciones Clientes se comunican con el servidor Qtopia Core usando memoria compartida. Detrás 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 comunicación entre los clientes y sus servidores en un

mínimo, resultando en una interfaz de usuario concisa. Las aplicaciones Qtopia Core normalmente usan

QPainter para dibujarse a sí mismas, pero también 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 señal 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 método estático

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 común: 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 función C++.

Varias variables de entorno afectan las aplicaciones Qtopia Core. Las más 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 aplicación usando el frame buffer

virtual de Qtopia (qvfb), una aplicación X11 que simula, pixel por pixel, el frame buffer actual. Para

habilitar el soporte a buffers virtuales en Qtopia Core, hay que pasar la opción –qvfb al script

configure. Sea consciente de que esta opción no está pensada para uso de producción. El frame buffer

virtual está localizado en tools/qvfb y puede ser invocado como sigue:

qvfb -width 320 -height 480 -depth 32

Otra opción que puede funcionar en la mayoría de las plataformas es usar VNC (Virtual Network

Computing) para ejecutar las aplicaciones remotamente. Para activar o habilitar el soporte VNC en Qtopia

266 21. Programación Embebida

Core, pasa la opción –qt –gfx –vnc a configure. Luego lanza tus aplicaciones Qtopia Core con la

opción de línea de comando –display VNC=0 y ejecuta un cliente VNC apuntando al host donde tu

aplicación este ejecutándose. El tamaño de la visualización 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 características que queremos dejar fuera para reducir

el consumo de memoria. Qtopia Core incluye más de un centenar de características configurables, cada una

de las cuales está asociada a un símbolo de pre procesamiento. Por ejemplo, QT_NO_FILEDIALOG excluye

QFileDialog de la librería QtGui, y QT_NO_I18N deja fuera todo el soporte para la

internacionalización. Las características están listadas en src/corelib/qfeatures.txt.

Qtopia Core provee cinco configuraciones de ejemplo (minimun, small, médium, large y dist)

que está alojado en los archivos src/corelib/qconfig_xxx.h. Estas configuraciones pueden ser

especificadas usando la opción –qconfig del script configure, por ejemplo:

./configure -qconfig small

Para crear configuraciones personalizadas, podemos proporcionar manualmente un archivo qconfig-

xxx.h y usarlo como si fuera una configuración estándar. 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, métodos de entrada y de estilos de decoraciones de

ventana, ejecuta el script configure con la opción –help.

El controlador de pantalla puede ser especificado usando la opción de línea de comando –display cuando

se inicia el servidor Qtopia Core, como se vio en la sección anterior, o estableciendo la variable de entorno

QWS_DISPLAY. El controlador del ratón 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 métodos de entrada y decoraciones son establecidos

programáticamente en el servidor usando QWSServer::setCurrentInputMethod() y

QApplication::qwsSetDecoration().

267 21. Programación Embebida

Los estilos de decoración 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

decoración 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 través 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 más rápido y usualmente más compacto que los formatos de vectores

tales como TTD y Type 1, si lo necesitamos solo a uno o dos tamaños 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 opción de línea 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 más rápido y más conveniente. Se espera que en una versión

futura de este libro se incluya más información en este tema.

268 Glosario

Glosario

Endiannes

El término inglés Endianness designa el formato en el que se almacenan los datos de más de un byte en un

ordenador. Big-endian es un método de ordenación en el que el byte de mayor peso se almacena en la

dirección más baja de memoria y el byte de menos peso en la dirección más alta. Little-endian es un método

de ordenación en el que el byte de mayor peso se almacena en la dirección más alta de memoria y el byte de

menos peso en la dirección más baja.

Mutex (Mutual Exclusión)

MUTEX es un sistema de sincronización de acceso múltiple para orígenes de información común (por medio

del mecanismo de cerrar y abrir: "lock-unlock").