Upload
marcosribeiro50
View
1.745
Download
23
Tags:
Embed Size (px)
Citation preview
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
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(“&”, “<”,“>”). 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 ®Exp)
{
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 “&” en una cadena:
str.replace("&", "&");
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 ®istro);
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 ®istro)
{
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 ®istro);
void antesDeInsertarPista(QSqlRecord ®istro);
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 ®istro)
{
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 ®istro)
{
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\">"
" 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.
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").