Está en la página 1de 268

2011

C++ - Programacin GUI con Qt 4

Jasmin Blanchete & Mark


Summerfield

Zona Qt
01/01/2011

6. Manejo de Layouts

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

C++ - Programacin GUI


con Qt 4

Jasmin Blanchete
Mark Summerfield

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

6. Manejo de Layouts

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

6. Manejo de Layouts

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


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

6. Manejo de Layouts

21. Programacin Embebida ............................................................................................. 264


Iniciando con Qtopia .................................................................................................. 264
Personalizando Qtopia Core ...................................................................................... 266
Glosario.............................................................................................................................. 268

6. Manejo de Layouts

Parte II
Qt Intermedio

6. Manejo de Layouts

6. Manejo de Layouts

Organizando Widgets en un Formulario

Layouts Apilados

Los Splitters (divisores o separadores)

reas de Desplazamiento

Barras de Herramientas y Dock Widgets

Interfaz de Mltiples Documentos

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

Organizando Widgets en un Formulario


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

6. Manejo de Layouts

Figura 6.1. El dilogo Buscar Archivo

El posicionamiento absoluto es la tcnica ms engorrosa para acomodar nuestros widgets. Esta tcnica se
basa en asignar tamaos y posiciones fijas a los widgets y al formulario. El constructor de
DialogoBuscarArchivo lucira de la siguiente manera:
DialogoBuscarArchivo::DialogoBuscarArchivo(QWidget *parent)
:QDialog(parent)
{

labelNombre->setGeometry(9, 9, 50, 25);


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

6. Manejo de Layouts

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

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

10

6. Manejo de Layouts

Las tres clases ms importantes son: QHBoxLayout, QVBoxLayout y QGridLayout. Estas heredan de
QLayout, la cual provee el marco bsico para las operaciones de layout. Las tres clases son totalmente
soportadas por Qt Designer y tambin pueden ser usadas directamente en el cdigo.
Este es el cdigo de DialogoBuscarArchivo usando administradores de layout:
DialogoBuscarArchivo::DialogoBuscarArchivo(QWidget *parent)
:QDialog(parent)
{

QGridLayout *layoutIzquierdo = new QGridLayout;


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

11

6. Manejo de Layouts

Figura 6.3. Organizacin de widgets del dilogo Buscar Archivo

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


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

12

6. Manejo de Layouts

Figura 6.4. El propsito de las diferentes polticas de tamao

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

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

13

6. Manejo de Layouts

Figura 6.5. QStackedLayout

Figura 6.6. Dos pginas del dialogo Preferencias

El dilogo Preferencias mostrado en la Figura 6.6 es un ejemplo del uso de QStackedLayout. Consiste
de un QListWidget a la izquierda y de un QStackedLayout a la derecha. Cada tem en el
QListWidget se corresponde con una pgina diferente del QStackedLayout. Este es el cdigo ms
relevante del constructor del dilogo:
DialogoPreferencias::DialogoPreferencias(QWidget *parent)
: QDialog(parent)
{

widgetLista = new QListWidget;


widgetLista->addItem(tr("Apariencia"));
widgetLista->addItem(tr("Explorador Web"));
widgetLista->addItem(tr("Correo y Noticias"));
widgetLista->addItem(tr("Avanzado"));
stackedLayout = new QStackedLayout;
stackedLayout->addWidget(paginaApariencia);
stackedLayout->addWidget(paginaExploradorWeb);
stackedLayout->addWidget(paginaCorreoYNoticas);
stackedLayout->addWidget(paginaAvanzado);
connect(widgetLista, SIGNAL(currentRowChanged(int)),
SLOT(setCurrentIndex(int)));

widgetLista->setCurrentRow(0);
}

stackedLayout,

14

6. Manejo de Layouts

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

Los Splitters (divisores o separadores)


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

separador.show();
return app.exec();
}

15

6. Manejo de Layouts

Figura 6.7. La aplicacin Separador

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


administradores de layouts, los cuales no tienen una representacin visual, QSplitter hereda de
QWidget, y por lo tanto puede ser usado como cualquier otro widget.
Figura 6.8. Widgets de la aplicacin Separador

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

16

6. Manejo de Layouts

Este es el cdigo del constructor de la ventana principal de la aplicacin Cliente de Correo:


ClienteCorreo::ClienteCorreo()
{

splitterDerecho = new QSplitter(Qt::Vertical);


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

La primera llamada a setStretchFactor() se realiza desde el splitter de la derecha


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

17

6. Manejo de Layouts

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

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

La manera de usar QScrollArea es llamando a la funcin setWidget() pasndole como argumento el


widget al que queremos dotar de barras de desplazamiento. QScrollArea automticamente cambia de
padre al widget para hacerlo hijo del viewport o puerto de vista (accesible a travs de

18

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

QScroolArea hereda mucho de su funcionalidad de QAbstractScrollArea. Las clases como


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

19

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 ttulo, aun cuando est anclado. Se pueden mover de un rea
a otra solo arrastrndolo desde su barra de ttulo. Tambin se puede separar del rea a donde est acoplado y
dejarlo como una ventana independiente y flotante con solo arrastrarlo fuera de cualquier rea de anclaje. Las
ventanas flotantes siempre se muestran encima de la ventana principal. Se pueden cerrar haciendo clic en el
botn de cerrar que se encuentra en la barra de ttulo de la misma. Cualquier combinacin de estas
caractersticas puede ser desactivada por medio de QDockWidget::setFeatures().
En versiones anteriores de Qt, las barras de herramientas eran tratadas como widgets acoplables y
compartan las mismas reas. Desde la versin 4 de Qt, las barras de herramientas ocupan sus propias reas
alrededor del widget central (como se muestra en la Figura 6.14) y no pueden ser desacopladas. Si se quiere
tener una barra de herramientas flotante, simplemente se agrega dentro de un QDockWindow.
Las esquinas indicadas con lneas punteadas pueden pertenecer a cualquiera de las dos reas contiguas. Por
ejemplo, podramos hacer que la esquina superior izquierda perteneciera al rea de anclaje izquierda
llamando al mtodoQMainWindow::setCorner(Qt::TopLeftCorner, Qt::LeftDockWidgetArea).
Figura 6.13. Un QMainWindow con un dock widget

20

6. Manejo de Layouts

Figura 6.14. Dock area y toolbar area de QMainWindow

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

21

6. Manejo de Layouts

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

Interfaz de Mltiples Documentos


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

22

6. Manejo de Layouts

}
Figura 6.16. La aplicacin MDI Editor

Figura 6.17. Mens de la aplicacin MDI Editor

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


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

23

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

24

6. Manejo de Layouts

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

menuVentana = menuBar()->addMenu(tr("&Ventana"));
menuVentana->addAction(actionCerrar);
menuVentana->addAction(actionCerrarTodos);
menuVentana->addSeparator();
menuVentana->addAction(actionMosaico);
menuVentana->addAction(actionCascada);
menuVentana->addSeparator();
menuVentana->addAction(actionSiguiente);
menuVentana->addAction(actionAnterior);
menuVentana->addAction(actionSeparador);

}
La funcin privada crearMenus() se encarga de agregar las acciones al men Ventana. Estas son las
acciones tpicas de este men y son implementadas a travs los slots closeActiveWindow(),
closeAllWindows(), tile(), y cascade() de la clase QWorkspace.

25

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

26

6. Manejo de Layouts

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


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

27

6. Manejo de Layouts

QString nombreArchivo = QFileDialog::getOpenFileName(this,


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

28

6. Manejo de Layouts

Este es el cdigo del archivo main.cpp:


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

29

7. Procesamiento de Eventos

Reimplementar Manejadores de Eventos

Instalando Filtros de Eventos

Evitar Bloqueos Durante Procesamientos Intensivos

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

Reimplementar Manejadores de Eventos


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

7. Procesamiento de Eventos

Ya hemos visto varios manejadores de eventos cuando implementamos algunos ejemplos en captulos
anteriores (MainWindow, EditorIcono y Plotter). Hay ms tipos de eventos enumerados en la
documentacin de QEvent y tambin es posible crear nuestros propios tipos de eventos y emitirlos por
nuestra cuenta. A continuacin, revisaremos los dos tipos de eventos que merecen una mayor explicacin: la
presin de teclas y los temporizadores.
Los eventos generados por la presin de teclas pueden ser controlados mediante la re implementacin de las
funciones keyPressEvent() y keyReleaseEvent(). En la seccin dedicada al widget Plotter, se
re implement este ltimo mtodo. Normalmente solo necesitaremos enfocarnos en keyPressEvent()
ya que las nicas teclas que nos interesara saber cuando se soltaron son: Ctrl , Shift y Alt (tambin llamadas
teclas modificadoras), y podemos obtener su estado por medio de QkeyEvent::modifiers(). Por
ejemplo, si estamos desarrollando un widget EditorCodigo, y nos interesa distinguir entre la presin de
la teclas Inicio y Ctr+Inicio, la re implementacin de keyPressEvent() se vera as:
void EditorCodigo::keyPressEvent(QkeyEvent *evento)
{
switch (evento->key()) {
case Qt::Key_Home:
if (evento->modifiers() & Qt::ControlModifier){
irPrincipioDeDocumento();
} else {
irPrincipioDeLinea();
}
break;
case Qt::Key_End:

default:
Qwidget::keyPressEvent(evento);
}
}
La presin de las teclas Tab y Shift+Tab son casos especiales. Estas son manejadas por
QWidget::event() pasando el enfoque al siguiente (o anterior) widget en el orden de tabulacin, antes
de llamar a keyPressEvent(). Este es el comportamiento habitual que queremos, pero para el widget
EditorCodigo, podramos preferir la tecla Tab idente una lnea. Aqu se muestra la re implementacin de
event():
bool EditorCodigo::event(QEvent *evento)
{
if (evento->type() == QEvent::KeyPress) {
QKeyEvent *eventoTecla =
static_cast<QKeyEvent*>(evento);
if (eventoTecla->key() == Qt::Key_Tab])
{
insertarEnPosicionActual(\t); return true;
}
}
return Qwidget::event(evento);
}
Si el evento fue emitido por la presin de una tecla, convertimos el objeto QEvent a QKeyEvent y
verificamos qu tecla ha sido presionada. Si fue la tecla Tab, realizamos algn tipo de procesamiento y
devolvemos true para comunicarle a Qt que el evento ya ha sido procesado. Si devolvemos false, Qt
propagar el evento al widget padre.
Un mtodo de alto nivel para implementar atajos de teclado es usar QAction. Por ejemplo, si las funciones
irPrincipioDeLinea() e irPrincipioDeDocumento() son slots pblicos del widget
EditorCodigo, y ste es usado como widget central en una clase MainWindow, podramos
implementar atajos de teclados con el siguiente cdigo:

31

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 fcil agregar los comandos al men o a la barra de herramientas, como se mostr en el
Capitulo 3. Si el comando no tiene que aparecer en la interfaz de usuario, podramos reemplazar el objeto
QAction con un QShortcut, la clase usada internamente por QAction para implementar atajos de
teclado.
De manera predeterminada, los atajos de teclado (ya sea usando QAction o QShortcut) estn habilitados
siempre y cuando la ventana que contiene al widget est activa. Este comportamiento puede ser cambiado
por medio de las funciones QAction::setShortcutContext() o QShortcut::setContext().
Otro tipo comn de evento es el emitido por los temporizadores. Mientras que la mayor parte de los eventos
ocurre como resultado de una accin del usuario, los temporizadores permiten a las aplicaciones ejecutar
procesos a intervalos de tiempo regulares. Se pueden usar para implementar el parpadeo del cursor y otras
animaciones, o simplemente refrescar un rea de la ventana.
Para demostrar el funcionamiento de los temporizadores, implementaremos el widget Ticker. Este widget
mostrar un texto que se ir desplazando un pixel a la izquierda cada 30 milisegundos. Si el widget es ms
ancho que el texto, este ltimo se repetir las veces que sea necesario hasta completar el ancho del widget.
Figura 7.1 El widget Ticker

Este es el archivo de cabecera del widget:


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

32

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

33

7. Procesamiento de Eventos

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

34

7. Procesamiento de Eventos

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

Instalar Filtros de Eventos


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

editNombre->installEventFilter(this);
editApellido->installEventFilter(this);
editCiudad->installEventFilter(this);
editTelefono->installEventFilter(this);
}
Una vez registrado el filtro de eventos, los eventos enviados a los widgets editNombre,
editApellido, editCiudad y editTelefono sern enviados primero a la funcin
eventFilter() de DialogoInfoCliente, antes que a su destino original.
Aqu vemos la implementacin de la funcin eventFilter() que recibe los eventos:
bool DialogoInfoCliente::eventFilter(QObject *target,
QEvent *evento)
{
if (target == editNombre || target == editApellido ||
target == editCiudad || target == editTelefono) {
if (evento->type() == QEvent::KeyPress) {
QKeyEvent *eventoTecla =static_cast<QKeyEvent *>
(evento);

35

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

36

7. Procesamiento de Eventos

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

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

Evitar Bloqueos Durante Procesamientos Intensivos


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

37

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 podra cerrar la ventana principal mientras la
aplicacin aun se encuentra realizando el guardado del archivo o volver a activar otra vez la misma accin
desde el men Archivo|Guardar, obteniendo un comportamiento indefinido e inesperado como resultado. La
solucin ms fcil a este problema es reemplazar
qApp->processEvents();
Con
qApp->processEvents(QEventLoop::ExcludeUserInputEvents);
De esta manera le informamos a Qt que ignore los eventos del teclado y del ratn.
Mientras se est ejecutando una operacin larga, es comn querer mostrar el progreso de la misma. Para esto
utilizamos QProgressDialog; esta clase posee una barra que mantiene al usuario informado sobre el
avance del proceso que est realizando la aplicacin y un botn que permite abortar la operacin en cualquier
momento. Este sera el cdigo para guardar un archivo de la aplicacin Hoja de Clculo usando
QProgressDialog y processEvents():
bool HojaCalculo::guardaArchivo(const QString &nombreArchivo)
{
QFile archivo(nombreArchivo);

QProgressDialog progreso(this);
progreso.setLabelText(tr(Guardando %1).arg(nombreArchivo));
progreso.setRange(0, CantidadFilas);
progreso.setModal(true);
for (int fila = 0; fila < CantidadFilas; ++fila) {
progreso.setValue(fila);
qApp->processEvents();
if (progreso.wasCanceled()) {
archivo.remove();
return false;
}
for (int columna = 0; columna < CantidadColumnas; ++columna) {
QString str = formula(fila, columna);
if (!str.isEmpty())
out<<quint16(fila)<<quint16(columna)<<str;
}
}
return true;
}

38

7. Procesamiento de Eventos

Creamos un QProgressDialog con la variable CantidadFilas como el nmero total de pasos a


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

39

8. Grficos En 2 y 3 Dimensiones

8. Grficos En 2 y 3 Dimensiones

Dibujando con QPainter

Transformaciones

Renderizado de Alta Calidad con QImage

Impresin

Grficos con Open GL

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

Dibujando con QPainter


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

pen(): propiedad de tipo QPen. Es usado para dibujar lineas y los bordes de figura. Consiste en
un color, un ancho, un estilo de linea, un estilo de cubierta y un estilo de unin.

40

8. Grficos En 2 y 3 Dimensiones

brush(): propiedad de tipo QBrush. Es un patrn usado para rellenar figuras geomtricas.
Normalmente consta de un color y un estilo, pero tambin puede ser una textura (una imagen que se
repite infinitamente) o un degradado.

font(): propiedad de tipo QFont, es usada para dibujar texto. La fuente tiene muchos atributos,
incluyendo una familia y un tamao de punto.

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

41

8. Grficos En 2 y 3 Dimensiones

Figura 8.2. Estilos de cubierta y de unin

Figura 8.3. Estilos de pluma

Figura 8.4. Estilos de pinceles predeterminados

Figura 8.5. Ejemplos de figuras geomtricas

42

8. Grficos En 2 y 3 Dimensiones

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

43

8. Grficos 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 cdigo:
QLinearGradient gradiente(50, 100, 300, 350);
gradiente.setColorAt(0.0, Qt::white);
gradiente.setColorAt(0.2, Qt::green);
gradiente.setColorAt(1.0, Qt::black);
Especificamos tres colores en tres posiciones diferentes entre los dos puntos de control. Las
posiciones se especifican como valores de punto flotante entre 0 y 1, donde el cero corresponde al
primer punto de control y el uno al segundo punto de control. Los colores establecidos sern
interpolados para formar el degradado.

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

Degradado cnico: esta definido por un punto central (x, y) y un ngulo . Los colores se esparcirn
alrededor del punto central siguiendo la direccin de las agujas del reloj.

Hasta ahora solo hemos usado solo tres propiedades de QPainter (pen(), brush() y font()).
QPainter posee ms propiedades que influencian la manera en que son dibujadas las figuras:

background(): es un propiedad de tipo QBrush que es usada para pintar el fondo de las figuras,
textos o imgenes cuando se establece la propiedad backgroundMode a Qt::OpaqueMode (por
defecto es Qt::TransparentMode).

brushOrigin(): es una propiedad de tipo QPoint que indica el punto desde donde se
comienza a aplicar un patrn de relleno.

clipRegion(): es una propiedad de tipo QRegion que marca el rea del dispositivo que puede
ser dibujada. Todo lo que se realice fuera de esta no tendr ningn efecto.

44

8. Grficos En 2 y 3 Dimensiones

viewport(), window() y worldTransform(): estas tres propiedades determinan cmo


QPainter mapea las coordenadas lgicas a las coordenadas fsicas del dispositivo. Por defecto,
estas coinciden. El sistema de coordenadas ser analizado en la prxima seccin.

compositionMode(): es una enumeracin que especifica cmo los nuevos pixeles dibujados
debern interactan con los ya existentes. Por defecto est en "source over", en donde los pixeles se
dibujan encima de los existentes. Esta caracterstica no est soportada por todos los dispositivos y la
veremos ms adelante en este captulo.

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

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

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

45

8. Grficos En 2 y 3 Dimensiones

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

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

46

8. Grficos 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 transformacin, es mas eficiente almacenarla en un objeto
QMatrix y asignarla a QPainter cada vez que la necesitemos.
Figura 8.9. El widget TempHorno

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

double GradosPorMinuto = 7.0;


double GradosPorSegundo = GradosPorMinuto / 60;
int MaxMinutos = 45;
int MaxSegundos = MaxMinutos * 60;
int IntervaloActualizacion = 1;

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

47

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

48

8. Grficos En 2 y 3 Dimensiones

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

49

8. Grficos En 2 y 3 Dimensiones

Ahora nos centraremos en el cdigo que dibuja el widget:


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

50

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

Renderizado de Alta Calidad con QImage


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

51

8. Grficos En 2 y 3 Dimensiones

En X11, algunas caractersticas, como son antialiasing y soporte para coordenadas fraccionales,
estn disponibles solo si la extensin X Render est presente en el servidor X.

En MacOs X, el motor grfico de antialiasing usa diferentes algoritmos que X11 y Windows para
dibujar polgonos, obteniendo resultados ligeramente diferentes.

Cuando la precisin es ms importante que la eficiencia, podemos dibujar en un QImage y copiar el


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

52

8. Grficos En 2 y 3 Dimensiones

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

Una caracterstica a tener en cuenta de la operacin QImage::CompositionMode_Xor es que tambin


se aplica al canal alfa. Esta hace que si se aplica a dos puntos blancos (0xFFFFFFFF), obtengamos un punto
transparente (0x00000000) en vez de uno negro (0xFF000000).

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

53

8. Grficos 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 cmo
imprimir un objeto QImage:
void VentanaImpresion::imprimirImagen(const QImage &imagen)
{
QPrintDialog dialogoImpresion(&impresora, this);
if (dialogoImpresion.exec()) {
QPainter painter(&impresora);
QRect rect = painter.viewport();
QSize tam = imagen.size();
tam.scale(rect.size(), Qt::KeepAspectRatio);
painter.setViewport(rect.x(), rect.y(),
tam.width(), tam.height());
painter.setWindow(imagen.rect());
painter.drawImage(0, 0, imagen);
}
}
Asumimos que la clase VentanaImpresion tiene una variable de tipo QPrinter llamada
impresora. Simplemente podramos haber creado el objeto QPrinter local al mtodo
imprimirImagen(), pero no tendramos forma de recordar las preferencias del usuario entre distintas
impresiones.

54

8. Grficos En 2 y 3 Dimensiones

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

Podemos convertir los datos a HTML e imprimirlos mediante el motor de texto enriquecido de Qt
(QTextDocument).

Podemos realizar el dibujo de las pginas manualmente.

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

55

8. Grficos En 2 y 3 Dimensiones

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

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

56

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

57

8. Grficos En 2 y 3 Dimensiones

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

void VentanaImpresion::imprimirPaginas(QPainter *painter,


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

58

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

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

59

8. Grficos En 2 y 3 Dimensiones

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

void VentanaImpresion::imprimirRecuadro(QPainter *painter,


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

60

8. Grficos En 2 y 3 Dimensiones

Grficos con OpenGL


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

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

61

8. Grficos 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 funcin QGLWidget::setFormat() para especificar las caractersticas
del contexto e inicializamos las variables privadas de la clase.
void Tetraedro::initializeGL()
{
glShadeModel(GL_FLAT);
glEnable(GL_DEPTH_TEST);
glEnable(GL_CULL_FACE);
}
La funcin initializeGL() es llamada solo una vez, antes de la llamada a paintGL(). Este es el
lugar en donde podemos configurar el contexto de renderizado de OpenGL, definiendo la lista de pantallas y
realizando otras inicializaciones.
Todo el cdigo est compuesto de llamadas a funciones de OpenGL, excepto por qglClearColor(). Si
queremos mantener todo en OpenGL estndar, podramos llamar a glClearColor() si trabajamos en
modo RGBA y glClearIndex() en modo color indexado.
void Tetraedro::resizeGL(int width, int height)
{
glViewport(0, 0, width, height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
GLfloat x = GLfloat(width) / height;
glFrustum(-x, x, -1.0, 1.0, 4.0, 15.0);
glMatrixMode(GL_MODELVIEW);
}
La funcin resizeGL() es llamada antes que se llame por primera vez a paintGL(), pero despus de la
llamada a initializeGL(). sta tambin es llamada cada vez que el widget cambia de tamao. Este es
el lugar en donde podemos configurar el "viewport" de OpenGL, las proyecciones y cualquier otro valor que
dependa del tamao del widget.
void Tetraedro::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
dibujar();
}
La funcin paintGL() es llamada cada vez que el widget necesite redibujarse. Esto es muy parecido a usar
QWidget::paintEvent(). El dibujo es realizado por la funcin privada dibujar().

62

8. Grficos En 2 y 3 Dimensiones

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

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

=
=
=
=

{
{
{
{

0.0, -1.0, +2.0 };


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

static const GLfloat * const coords[4][3] = {


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

void Tetraedro::mousePressEvent(QMouseEvent *event)


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

63

8. Grficos En 2 y 3 Dimensiones

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


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

64

8. Grficos 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 mdulo QtOpenGL de la aplicacin con las libreras de OpenGL, necesitamos agregar la
siguiente entrada al archivo .pro
QT += opengl
Con esto completamos la aplicacin Tetraedro. Para obtener mas informacin sobre el mdulo QtOpenGL,
consulte la documentacin de referencia de QGLWidget, QGLFormat, QGLContext, QGLColormap y
QGLPixelBuffer.

65

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

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


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

66

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

67

9. Arrastrar y Soltar

Figura 9.1. La aplicacin Selector de Proyecto

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

68

9. Arrastrar y Soltar

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

69

9. Arrastrar y Soltar

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

Soporte de Tipos de Arrastre Personalizados


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

utilizando

2. Podemos hacer una subclase QMimeData y reimplementar los mtodos formats() y


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

70

9. Arrastrar y Soltar

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

71

9. Arrastrar y Soltar

}
Las funciones toCsv() y toHTML() convierten una cadena de "etiquetas y saltos de lnea" en un archivo
CSV (valores separados por comas) o una cadena HTML. Por ejemplo, los datos:
Red Green Blue
Cyan Yellow Magenta
Son convertidos en:
"Red", "Green", "Blue"
"Cyan", "Yellow", "Magenta"
O a:
<table>
<tr><td>Red<td>Green<td>Blue
<tr><td>Cyan<td>Yellow<td>Magenta
</table>
La conversin se realiza en la forma ms sencilla posible, usando QString::replace(). Para evitar los
caracteres especiales de HTML, podemos usar Qt::escape().
void MyTableWidget::dropEvent(QDropEvent *event)
{
if (event->mimeData()->hasFormat("text/csv")) {
QByteArray csvData = event->mimeData()->data("text/csv");
QString csvText = QString::fromUtf8(csvData);

event->acceptProposedAction();
} else if (event->mimeData()->hasFormat("text/plain")) {
QString plainText = event->mimeData()->text();

event->acceptProposedAction();
}
}
Aunque se incluyen los datos en tres formatos diferentes, aceptamos solamente dos de ellos en
dropEvent(). Si el usuario arrastra las celdas de un QTableWidget a un editor HTML, queremos que
las celdas se conviertan en una tabla HTML. Pero si el usuario arrastra HTML arbitrario en un
QTableWidget, no vamos a querer aceptarlo.
Para que este ejemplo funcione, tambin tenemos que llamar a setAcceptDrops(true) y
setSelectionMode(ContiguousSelection) en el constructor de MyTableWidget.
A continuacin, vamos a realizar nuevamente el ejemplo, pero esta vez vamos a hacer una subclase de
QMimeData para posponer o evitar las conversiones potencialmente costosas entre
QTableWidgetItems y QByteArray. Aqu est la definicin de nuestra subclase:
class TableMimeData : public QMimeData
{
Q_OBJECT
public:
TableMimeData(const QTableWidget *tableWidget,
const QTableWidgetSelectionRange &range);
const QTableWidget *tableWidget() const { return myTableWidget; }
QTableWidgetSelectionRange range() const { return myRange; }
QStringList formats() const;
protected:
QVariant retrieveData(const QString &format,
QVariant::Type preferredType) const;
private:
static QString toHtml(const QString &plainText);

72

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

73

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 funcin dropEvent() es similar a la que haba anteriormente en esta seccin, pero esta vez la
optimizamos comprobando primero si podemos convertir con seguridad el objeto QMimeData a un
TableMimeData. Si la instruccin qobject_cast <T>() funciona, significa que el arrastre se origin
por un objeto de tipo MyTableWidget en la misma aplicacin, y podemos acceder directamente a los
datos de tabla en vez de ir a travs de la API de QMimeData. Si la instruccin falla, extraemos los datos de
la forma estndar.
En este ejemplo, codificamos el texto CSV con la codificacin UTF-8. Si queremos estar seguros de utilizar
la codificacin correcta, podramos utilizar el parmetro charset del tipo MIME text/plain para
especificar una codificacin explcita. Aqu estn algunos ejemplos:
text/plain;charset=US-ASCII
text/plain;charset=ISO-8859-1
text/plain;charset=Shift_JIS
text/plain;charset=UTF-8

Manejo del Portapapeles


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

74

9. Arrastrar y Soltar

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

75

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

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

76

10. Clases para Visualizar Elementos (Clases Item View)

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

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

Usando las Clases Item View de Qt


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

};

77

10. Clases para Visualizar Elementos (Clases Item View)

Figura 10.3. La aplicacin Seleccionador de Simbolos

Al constructor del dialogo le debemos pasar un objeto QMap<int,QString>. Podemos recuperar el


identificador de un elemento por medio de la funcin idSeleccionado() (que devolver -1 si no hay
ninguno seleccionado).
SeleccionaSimboloDiagramaFlujo::SeleccionaSimboloDiagramaFlujo(
const QMap<int, QString> &mapSimbolo, QWidget *parent):
QDialog(parent)
{
id = -1;
widgetLista = new QListWidget;
widgetLista->setIconSize(QSize(60, 60));
QMapIterator<int, QString> it(mapSimbolo);
while (it.hasNext()) {
it.next();
QListWidgetItem *item = new QListWidgetItem( it.value(),
widgetLista);
item->setIcon(iconoDeSimbolo(it.value()));
item->setData(Qt::UserRole, it.key());
}

}
Comenzamos por inicializar la variable id (que nos indica el ultimo identificador seleccionado) a -1. A
continuacin creamos un QListWidget. Mediante un iterador recorremos los elementos de la lista de
smbolos incluidos en el QMap creando un QListWidgetItem para cada uno. El constructor de
QListWidgetItem toma como argumento un QString que representa el texto a mostrar, seguido por el
QListWidget padre.
Despus establecemos el icono del QListWidgetItem y llamamos a setData() para agregarle un
identificador al mismo. La funcin privada iconoDeSimbolo() devuelve un objeto QIcon perteneciente
a un elemento determinado.
La clase QListWidgetItem tiene varios roles, cada uno de los cuales tiene un dato asociado de tipo
QVariant. Los roles ms comunes son Qt::DisplayRole, Qt::EditRole y Qt::IconRole, y
cada uno de estos tiene funciones de lectura y escritura propias (como setText(), setIcon(), etc),
pero adems de estos existen otros roles. Tambin podemos definir roles personales especificando un valor
numrico mayor o igual a Qt::UserRole. En nuestro ejemplo usamos Qt::UserRole para almacenar
el identificador de cada elemento.

78

10. Clases para Visualizar Elementos (Clases Item View)

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

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

79

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

80

10. Clases para Visualizar Elementos (Clases Item View)

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

Aqu mostramos un extracto del cdigo del constructor:


VisorConfiguraciones::VisorConfiguraciones(QWidget *parent):
QDialog(parent)
{
organizacion = "Trolltech";
aplicacion = "Designer";
widgetArbol = new QTreeWidget;
widgetArbol->setColumnCount(2);
widgetArbol->setHeaderLabels(QStringList() << tr("Clave")
<< tr("Valor"));
widgetArbol->header()->setResizeMode(0,
QHeaderView::Stretch);
widgetArbol->header()->setResizeMode(1,
QHeaderView::Stretch);

setWindowTitle(tr("Visor de Configuraciones"));
leerConfiguraciones();
}
Para acceder a las configuraciones de la aplicacin, debemos crear un objeto QSettings pasndole el
nombre de la organizacin y el de la aplicacin como parmetros, para esto utilizamos las variables
organizacion y aplicacion. Luego creamos un nuevo QTreeWidget y llamamos a la funcin
leerConfiguraciones().
void VisorConfiguraciones::leerConfiguraciones()
{
QSettings config(organizacion, aplicacion);

81

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

82

10. Clases para Visualizar Elementos (Clases Item View)

Usando Modelos Predefinidos


Qt provee varios modelos predefinidos para utilizar con las vistas:
QStringListModel

Almacena una lista de cadenas de caracteres

QStandardItemModel

Almacena datos jerrquicos 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 forneas

QSortFilterProxyModel

Ordena y/o filtra los datos contenidos en otros modelos

En esta seccin, veremos como usar las clases QStringListModel, QDirModel y


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

A continuacin mostramos un extracto del constructor:


DialogoLiderEquipo::DialogoLiderEquipo(const
QStringList &lideres, QWidget *parent)
:QDialog(parent)
{
modelo = new QStringListModel(this);
modelo->setStringList(lideres);
viewLista = new QListView;
viewLista->setModel(modelo);
viewLista->setEditTriggers(QAbstractItemView::AnyKeyPressed
| QAbstractItemView::DoubleClicked);

}
Comenzamos por crear y rellenar un QStringList. A continuacin creamos un QListView y
establecemos su modelo. Tambin activamos algunos disparadores de edicin que le permitirn al usuario

83

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

84

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 aplicacin Visor de Directorios

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

85

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

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

86

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 construccin
del QSortFilterProxyModel. Asignamos el modelo a usar por medio de setSourceModel() y le
decimos que el filtro lo aplique sobre la columna 0 del modelo original. La funcin
QComboBox::addItem() acepta un argumento opcional "data" de tipo QVariant; usamos este para
almacenar el valor QRegExp::PatternSyntax que corresponde a cada elemento del mismo.
void DialogoNombreColores::reaplicarFiltro()
{
QRegExp::PatternSyntax sintaxis = QRegExp::PatternSyntax
(comboSintaxis->itemData(comboSintaxis->currentIndex()).toInt());
QRegExp regExp(lineEditFiltro->text(), Qt::CaseInsensitive,
sintaxis);
modeloProxy->setFilterRegExp(regExp);
}
El slot reaplicarFiltro() es invocado cada vez que el usuario modifica la cadena de filtro o el
elemento seleccionado en el combo. Creamos un objeto QRegExp usando el texto contenido en el
QLineEdit. Luego establecemos su patrn de sintaxis al valor seleccionado en el combo. Cuando
llamamos a setFilterRegExp(), el nuevo filtro se transforma en activo y la vista es actualizada
automticamente.

Implementando Modelos Personalizados


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

87

10. Clases para Visualizar Elementos (Clases Item View)

Figura 10.9. Vista esquemtica de los modelos de Qt

Para un modelo de tipo lista, el nico componente relevante del ndice es el nmero de fila, asequible por
medio de QModelIndex::row(). Para un modelo de tipo tabla, en cambio los componentes relevantes
del ndice son el nmero de fila y el de columna, dados por fromQModelIndex::row() y
QModelIndex::column() respectivamente. Para ambos tipos de modelos, el padre de cada elemento es
el elemento raz, el cual es representado por medio de un ndice invlido. Los dos primeros ejemplos de esta
seccin muestran cmo crear y utilizar modelos de tipo tabla.
Un modelo de tipo rbol es similar a uno tipo tabla con las siguientes diferencias:

Al igual que en las tablas, el padre de los elementos de ms alto nivel es el elemento raz, pero cada
padre de los restantes elementos es un elemento vlido en la jerarqua.

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


Cada elemento tiene aparte de sus datos, cero o ms hijos.

Dado que cada elemento es capaz de tener otros elementos como hijos, es posible representar
estructuras de datos recursivas, como mostraremos en el ejemplo final de esta seccin.

Nuestro primer ejemplo es un modelo de solo lectura que muestra una tabla con las monedas del
mundo y la relacin de cambio que existe entre las mismas.

Figura 10.10. La aplicacin Monedas Circulantes

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

88

10. Clases para Visualizar Elementos (Clases Item View)

La clase ModeloMonetario ser usada con un QTableView comn. El modelo almacena los valores en
un QMap<QString,double>; cada clave es un cdigo monetario y cada valor corresponde al tipo de
cambio de la moneda con respecto al dolar norteamericano. El siguiente fragmento de cdigo muestra cmo
se cargan los datos en el QMap y cmo se usa el modelo:
QMap<QString, double> mapMonedas;
mapMonedas.insert("AUD", 1.3259);
mapMonedas.insert("CHF", 1.2970);

mapMonedas.insert("SGD", 1.6901);
mapMonedas.insert("USD", 1.0000);
ModeloMonetario modeloMonedas;
modeloMonedas.setMonedas(mapMonedas);
QTableView ViewTabla;
ViewTabla.setModel(&modeloMonedas);
ViewTabla.setAlternatingRowColors(true);
Ahora revisaremos la implementacin del modelo, comenzando por el archivo cabecera:
class ModeloMonetario : public QAbstractTableModel
{
public:
ModeloMonetario(QObject *parent = 0);
void setMonedas(const QMap<QString, double> &valores);
int rowCount(const QModelIndex &parent) const;
int columnCount(const QModelIndex &parent) const;
QVariant data(const QModelIndex &index, int role) const;
QVariant headerData(int seccion, Qt::Orientation orientacion,
int rol) const;
private:
QString monedaAt(int desp) const;
QMap<QString, double> mapMonedas;
};
Hemos escogido que nuestro modelo herede de QAbstractTableModel, ya que su comportamiento est
muy cerca del que buscamos. Qt provee varios modelos bsicos, incluyendo QAbstractListModel,
QAbstractTableModel y QAbstractItemModel. La clase QAbstractItemModel es usada
como soporte de varios tipos de modelos, incluyendo aquellos basados en datos recursivos, mientras que las
clase QAbstractListModel y QAbstractTableModel son ms convenientes para conjuntos de
datos unidimensionales o bidimensionales.
Figura 10.11. Arbol de herencia de las clases de modelos abstractos

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

89

10. Clases para Visualizar Elementos (Clases Item View)

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

90

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

Aqu mostramos cmo configurar y usar el modelo:


QStringList ciudades;
ciudades << "Arvika" << "Boden" << "Eskilstuna" << "Falun"
<< "Filipstad" << "Halmstad" << "Helsingborg"
<<"Karlstad"<< "Kiruna" << "Kramfors" << "Motala"
<< "Sandviken"<< "Skara" << "Stockholm"
<< "Sundsvall" << "Trelleborg";
ModeloCiudad modelo;
modelo.setCiudades(ciudades);

91

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

92

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

93

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

94

10. Clases para Visualizar Elementos (Clases Item View)

La aplicacin Analizador Expreg consta de cuatro clases:

VentanaExpReg: es una ventana que le permite al usuario ingresar una expresin regular y
muestra el correspondiente rbol de anlisis.

AnalizadorExpReg: genera el rbol de anlisis a partir de una expresin regular.

ModeloExpReg: es un modelo jerrquico que encapsula el rbol de anlisis.

Nodo: representa un elemento del rbol de anlisis.


Figura 10.14. La aplicacin Analizador Expreg

Empecemos con la clase Nodo:


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

95

10. Clases para Visualizar Elementos (Clases Item View)

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

96

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

97

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

98

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

Implementando Delegados Personalizados


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

99

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 creacin y el llenado de un table widget:
EditorPistas::EditorPistas(QList<Pista> *pistas,
QWidget *parent): QDialog(parent)
{
this->pistas = pistas;
tableWidget = new QTableWidget(pistas->count(), 2);
tableWidget->setItemDelegate(new PistaDelegate(1));
tableWidget->setHorizontalHeaderLabels(
QStringList() << tr("Pista") << tr("Duracion"));
for (int fila = 0; fila < filas->count(); ++fila) {
Pista pista = pistas->at(fila);
QTableWidgetItem *item0 = new QTableWidgetItem(pista.titulo);
tableWidget->setItem(fila, 0, item0);
QTableWidgetItem *item1 = new QTableWidgetItem
(QString::number(pista.duracion));
item1->setTextAlignment(Qt::AlignRight);
tableWidget->setItem(fila, 1, item1);
}

}
El constructor crea un table widget, y en lugar de usar el delegado predeterminado de este, establecemos
nuestro propio PistaDelegate, pasndolo en la columna que contiene el dato de los tiempos de duracin.
Comenzamos estableciendo los encabezados de las columnas, luego iteramos sobre los datos, llenando las
filas con el nombre y la duracin de cada pista.
El resto del constructor y el resto del dialogo EditorPista no contiene ninguna sorpresa, as que veremos
ahora la clase PistaDelegate que maneja el dibujado y la edicin de los datos de una pista.
class PistaDelegate : public QItemDelegate
{
Q_OBJECT
public:
PistaDelegate(int columnaDuracion, QObject *parent = 0);
void paint(QPainter *painter,const QStyleOptionViewItem &option,
const QModelIndex &index) const;

100

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

101

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

102

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

103

11. Clases Contenedoras

11. Clases Contenedoras

Contenedores Secuenciales

Contenedores Asociativos

Algoritmos Genricos

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

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

104

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++ comn es que un vector posee su
propio tamao y puede ser redimensionado. Anexar elementos extras al final de un vector resulta ser muy
eficiente, mientras que la insercin de elementos al principio o en el medio de un vector puede ser algo
costoso.
Figura 11.1. Un vector de doubles
0

937.81

25.984

308.74

310.92

40.9

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

105

11. Clases Contenedoras

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

106

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

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

107

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

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

108

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

109

11. Clases Contenedoras

for (int i = 0; i < 360; ++i)


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

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

110

11. Clases Contenedoras

Cmo Funciona La Comparticin Implcita (Implicit Sharing)


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

111

11. Clases Contenedoras

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

22 350 000

Seoul

22 050 000

Tokyo

34 000 000

Una manera sencilla de insertar elementos en un mapa es llamando al mtodo insert():


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

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

112

11. Clases Contenedoras

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

113

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 funcin setValue() que modificaa el valor asociado con el elemento
actual:
QMutableMapIterator<QString, int> i(mapa);
while (i.hasNext()) {
i.next();
if (i.value() < 0.0)
i.setValue(-i.value());
}
Los iteradores estilo STL tambin proporcionan las funciones key() y value(). Con los tipos de
iteradores no constantes, la funcin value() retorna una referencia no constante, permitindonos cambiar
el valor as como lo iteramos. Nota, que aunque estos iteradores son llamados de estilo STL, estos se
desvan significantemente de los iteradores map<K, T> de STL, los cuales estn basados en la plantilla
pair<K, T>.
El ciclo foreach tambin funciona sobre contenedores asociativos, pero solamente en el componente valor
del par compuesto por la clave y el valor (clave-valor). Si necesitamos la componente clave y tambin la
componente valor, podemos llamar las funciones keys() y values(cosnt K &) en ciclos foreach
anidados:
QMultiMap<QString, int> mapa;
...
foreach (QString clave, map.keys()) {
foreach (int valor, map.values(clave)) {
do_something(clave, valor);
}
}

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

114

11. Clases Contenedoras

QStringList::iterator j = qFind(lista.begin(), lista.end(), "Petra");


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

115

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. Despus
de la llamada a esta funcin, aun debe hacerse el llamado a la funcin clear(). Por ejemplo:
qDeleteAll(lista);
ista.clear();
El algoritmo qSwap() intercambia el valor de dos variables. Por ejemplo:
int x1 = linea.x1();
int x2 = linea.x2();
if (x1 > x2)
qSwap(x1, x2);
Finalmente, la cabecera <QtGlobal>, la cual es incluida en otras cabeceras de Qt, proporciona muchas
definiciones tiles, incluyendo la funcin qAbs(), que retorna el valor absoluto de sus argumentos, y las
funciones qMin() y qMax(), que retornan el mnimo y el mximo de dos valores.

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


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

116

11. Clases Contenedoras

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

117

11. Clases Contenedoras

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

\t

E L

\n P E R R O

\n

La cadena retornada por la funcin trimmed() es:


B O B

\t

E L

\n P E R R O

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

118

11. Clases Contenedoras

QString str = " BOB \t EL \nPERRO \n";


qDebug() << str.simplified();
La cadena retornada por simplified() es:
B O B
Una cadena puede ser
QString::split():

dividida

E L

en

un

P E R R O

QStringList

formado

por

subcadenas

usando

QString str = "hay que pagar la renta";


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

el

macro

qPrintable()

que

realiza

lo

mismo

que

printf("Usuario: %s\n", qPrintable(str));


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

119

11. Clases Contenedoras

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

120

11. Clases Contenedoras

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

121

12. Entrada/Salida

12. Entrada/Salida

Lectura y Escritura de Datos Binarios

Lectura y Escritura de Archivos de Texto

Navegar por Directorios

Incrustando Recursos

Comunicacin entre Procesos

La lectura o escritura de archivos u otros dispositivos es una necesidad comn en casi todas las aplicaciones.
Qt provee un excelente soporte para las operaciones de E/S a travs de QIODevice, una poderosa
abstraccin que encapsula "dispositivos" que pueden leer y escribir bloques de bytes. Qt incluye las
siguientes clases derivadas de QIODevice:
QFile

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 comunicacin entre procesos.

QTcpSocket

Transfiere un flujo de datos sobre una red usando TCP.

QUdpSocket

Enva o recibe datagramas UDP sobre la red.

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

122

12. Entrada/Salida

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

Lectura y Escritura de Datos Binarios


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

123

12. Entrada/Salida

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

124

12. Entrada/Salida

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

125

12. Entrada/Salida

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

126

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 funcin write() para que lo guarde en otro archivo. Tener
todos los datos en un QByteArray requiere ms memoria que ir leyendo los elementos uno a uno, pero
esto ofrece algunas ventajas. Por ejemplo, podemos usar qCompress() y qUncompress() para
comprimir y descomprimir datos.
Hay otros escenarios en donde acceder a QIODevice directamente es ms apropiado que usar
QDataStream. La clase QIODevice provee la funcin peek() que devuelve el siguiente byte de datos
sin efectos secundarios mientras que la funcin getChar() transforma el byte en un carcter. Esto
funciona para dispositivos de acceso aleatorio (como archivos) as coma tambin para dispositivos
secuenciales (como sockets de red). Tambin disponemos de la funcin seek() que establece la posicin
del dispositivo, para aquellos que soporten acceso aleatorio.
Los archivos binarios proveen las forma ms verstil y compacta de almacenar datos, y QDataStream hace
que el acceso a datos binarios sea muy fcil. Adems de los ejemplos dados en esta seccin, hemos visto el
uso de QDataStream en el Capitulo 4 para leer y escribir los archivos de la hoja de calculo y lo veremos
de nuevo en el Capitulo 19 para cargar y guardar cursores de Windows.

Lectura y Escritura de Archivos de Texto


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

127

12. Entrada/Salida

Si el objeto salida es un QTextStream, el valor que se guardar ser "NoruegaSuecia". No podemos


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

Auto-detectccion basada en prefijo (cuando lee)


Binary

Octal

10
16

Decimal
Hexadecimal

setNumberFlags(NumberFlags)
showBase
ForceSign
ForcePoint
UppercaseBase

Muestra un prefijo esi la base es 2 (0b), 8


(0) o 16 (0x)
Siempre muestra el signo en nmeros reales
Siempre coloca el separador decimal en
nmeros
Usa versiones en maysculas de base prefijada
(0X, 0B)

128

12. Entrada/Salida

UppercaseDigits

Usa letras maysculas en nmeros


hexadecimales

setRealNumberNotation(RealNumberNotation)
FixedNotation

Notacin de Punto-fijo (p. e., 0.000123)

ScientificNotation

Notacin cientfica (p. e., 1.234568e-04)


Notacin de punto-fijo o cientfica, la que sea
ms compacta

SmartNotation
setRealNumberPrecision(int)

Establece el nmero mximo de dgitos que deben ser generados (6 por defecto)
setFieldWidth(int)
Establece el tamao mnimo de un campo (0 por defecto)
setFieldAlignment(FieldAlignment)
AlignLeft

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 nmero

setPadChar(QChar)
Establece el caracter usado para los campos de relleno (espacio por defecto)
Al igual que QDataStream, QTextStream opera sobre una subclase de QIODevice, pudiendo ser
QFile, QTemporaryFile, QBuffer, QProcess, QTcpSocket, o QUdpSocket.
Adems puede ser usada directamente sobre un objeto QString, por ejemplo:
QString str;
QTextStream(&str) << oct << 31 << " " << dec << 25 << endl;
Esto hace que el contenido de la cadena sea "37 25/n", ya que el numero decimal 31 es expresado como 37
en octal. En este caso, no necesitamos establecer la codificacin del flujo, ya que QString siempre trabaja
en Unicode.
Veamos un ejemplo simple de un formato de archivo basado en texto. En la aplicacin Hoja de Calculo
descrita en la Parte I, usamos un formato binario para almacenar los datos. Estos consistan en una secuencia
de fila, columna, formula, uno por cada celda no vaca. Guardar estos datos como texto es sencillo; aqu
vemos un extracto de una versin modificada del mtodo para guardar el archivo de la aplicacin Hoja de
Calculo, en el mtodo writeFile():
QTextStream salida(&archivo);
for (int fila = 0; fila < RowCount; ++fila) {
for (int columna = 0; columna < ColumnCount; ++columna) {
QString str = formula(fila, columna);
if (!str.isEmpty())
salida << fila << " " << columna
<< " " << str << endl;
}
}

129

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

130

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

131

12. Entrada/Salida

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

Navegar por Directorios


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

132

12. Entrada/Salida

QDir::convertSeparators() para convertir el separador de directorios al utilizado en cada


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

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

133

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 estticamente). Esta tcnica tiene dos desventajas: si
necesitamos hacer cambios en algn dato embebido, debemos cambiar tambin el ejecutable, y el tamao del
ejecutable ser ms grande porque debe alojar los datos incrustados.
El sistema de recursos de Qt provee ms caractersticas que las presentadas en este ejemplo, incluye soporte
para
alias
de
archivos
y
para
localizacin.
Todo
esto
est
documentado
en
http://doc.trolltech.com/4.1/resources.html.

Comunicacin entre Procesos


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

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

134

12. Entrada/Salida

void procesoError(QProcess::ProcessError error);


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

135

12. Entrada/Salida

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

136

12. Entrada/Salida

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

137

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 comunicacin entre varias aplicaciones
en vez de ejecutarlas, podra ser mejor comunicarse directamente con ellas, usando las clases de trabajo en
red de Qt o la extensin ActiveQt si estamos trabajando en Windows.

138

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 integracin con bases de datos a la interfaz de usuario. Este
captulo est vinculado con las clases de modelo/vista de Qt cubierto en el Captulo 10.
Una conexin de base de datos es representada a travs de un objeto QSqlDatabase. Qt usa controladores
para comunicarse con las distintas APIs de bases de datos. La Edicin de Escritorio de Qt incluye los
siguientes controladores:

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

139

13. Bases de Datos

Conectando y Consultando
Para ejecutar una consulta SQL, primero debemos establecer una conexin con la base de datos. Como es
tpico, las conexiones a las bases de datos son establecidas en una funcin separada que llamaremos al inicio
de la aplicacin. Por ejemplo:
bool crearConexion()
{
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
db.setHostName("mozart.konkordia.edu");
db.setDatabaseName("musicdb");
db.setUserName("gbatstone");
db.setPassword("T17aV44");
if (!db.open()) {
QMessageBox::critical(0, QObject::tr("Error de Base de
Datos"), db.lastError().text());
return false;
}
return true;
}
Primero llamamos a QSqlDatabase::addDatabase() para crear un objeto QSqlDatabase. El
primer argumento a addDatabase() especifica cul controlador de base de datos debe usar Qt para
acceder a la base de datos. En este caso, usamos MySQL.
Lo siguiente que se hace es establecer el nombre de host de la base de datos, el nombre de la base de datos, el
nombre de usuario y la contrasea, para luego abrir la conexin. Si open() falla, mostraremos un mensaje
de error.
Tpicamente, llamaramos a crearConexion() en la funcin main():
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
if (!crearConexion())
return 1;

return app.exec();
}
Una vez que una conexin es establecida, podemos usar QSqlQuery para ejecutar cualquier consulta SQL
que la base de datos usada soporte. Por ejemplo, aqu se muestra como ejecutar una sentencia SELECT:
QSqlQuery query;
query.exec("SELECT titulo, ao FROM cd WHERE ao >= 1998");
Despus del llamado a exec(), podemos navegar a travs del conjunto de resultados de la consulta:
while (query.next()) {
QString titulo = query.value(0).toString();
int ao = query.value(1).toInt();
cerr << qPrintable(titulo) << ": " << ao << endl;
}
Llamamos a next() una vez para posicionar el QSqlQuery en el primer registro del conjunto de
resultados. Llamadas subsecuentes a next() avanzan el puntero un registro a la vez, hasta que el final sea
alcanzado, en donde next() retorna false. Si el conjunto de resultados est vaco (o si la consulta falla),
el primer llamado a next() retornar false.

140

13. Bases de Datos

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

141

13. Bases de Datos

142

Despus del llamado a exec(), podemos llamar a bindValue() o a addBindValue() para sustituir
nuevos valores, luego llamar a exec() de nuevo para ejecutar el query con los nuevos valores.
Los sostenedores de espacio son usados muy a menudo para especificar datos binarios o cadenas que
contengan caracteres que no sean ASCII o que no sean Latin-1. Tras bastidores, Qt usa el formato Unicode
con aquellas bases de datos que soporten lo soporten, y para aquellas donde no, Qt convierte las cadenas a la
codificacin apropiada transparentemente.
Qt soporta transacciones SQL sobre las bases de datos donde stas estn disponibles. Para iniciar una
transaccin, llamamos al mtodo transaction() sobre el objeto que represente la conexin a la base de
datos. Para finalizar la transaccin, llamamos a commit() o a rollback(). Por ejemplo, aqu se muestra
cmo haramos para buscar una clave fornea y ejecutar una sentencia INSERT dentro de una transaccin:
QSqlDatabase::database().transaction();
QSqlQuery query;
query.exec("SELECT id FROM artista WHERE nombre = Gluecifer");
if (query.next()) {
int artistaId = query.value(0).toInt();
query.exec("INSERT INTO cd (id, artistaid, titulo, ao) "
"VALUES (201, " + QString::number(artistaId)
+ ", Riding the Tiger, 1997)");
}
QSqlDatabase::database().commit();
La funcin QSqlDatabase::database() retorna un objeto QSqlDatabase representando la
conexin que hemos creado en crearConexion(). Si la transaccin no pudo iniciarse,
QSqlDatabase::transaction() retorna false. Algunas bases de datos no soportan las
transacciones. Para ellas, las funciones transaction(), commit() y rollback() no hacen nada.
Podemos probar si una base de datos soporta transacciones usando hasFeature() sobre el QSqlDriver
asociado con la base de datos:
QSqlDriver *driver = QSqlDatabase::database().driver();
if (driver->hasFeature(QSqlDriver::Transactions))

Las caractersticas de muchas otras bases de datos pueden ser probadas, incluyendo si la base de datos
soporta BLOBs (Binary Large Objects), Unicode y consultas preparadas.
En los ejemplos que hemos visto hasta ahora hemos asumido que la aplicacin est usado una sola conexin
a la base de datos. Si queremos crear mltiples conexiones, podemos pasar un nombre como segundo
argumento a addDatabase(). Por ejemplo:
QSqlDatabase db = QSqlDatabase::addDatabase("QPSQL", "OTHER");
db.setHostName("saturn.mcmanamy.edu");
db.setDatabaseName("starsdb");
db.setUserName("hilbert");
db.setPassword("ixtapa7");
Luego podemos recuperar un puntero
QSqlDatabase::database():

al

objeto

QSqlDatabase

pasndole

el

nombre

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

13. Bases de Datos

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

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

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

143

13. Bases de Datos

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

144

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

145

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 aplicacin CD Collection

Figura 13.2. Tablas de la aplicacin CD Collection

Figura 13.3. El dialogo ArtistForm

146

13. Bases de Datos

El constructor es muy similar a uno que se usara para crear un formulario basado en un modelo que no sea
SQL:
ArtistForm::ArtistForm(const QString &nombre, QWidget *parent)
: QDialog(parent)
{
modelo = new QSqlTableModel(this);
modelo->setTable("artista");
modelo->setSort(Nombre_Artista, Qt::AscendingOrder);
modelo->setHeaderData(Nombre_Artista, Qt::Horizontal,
tr("Nombre"));
modelo->setHeaderData(Pais_Artista, Qt::Horizontal,
tr("Pais"));
modelo->select();
connect(modelo, SIGNAL(beforeInsert(QSqlRecord &)),
this, SLOT(AntesDeInsertarArtista
(QSqlRecord &)));
tableView = new QTableView;
tableView->setModel(modelo);
tableView->setColumnHidden(Id_Artista, true);
tableView->setSelectionBehavior
(QAbstractItemView::SelectRows);
tableView->resizeColumnsToContents();
for (int fila = 0; fila < modelo->rowCount(); ++fila) {
QSqlRecord registro = modelo->record(fila);
if (registro.value(Nombre_Artista).toString() ==
nombre) {
tableView->selectRow(fila);
break;
}
}

}
Comenzamos el constructor con la creacin de un QSqlTableModel. Pasamos el puntero this como
padre para darle propiedad del formulario. Hemos elegido ordenar por la columna 1 (especificado por la
constante Nombre_Artista), la cual corresponde al campo nombre. Si no especificamos las cabeceras
de las columnas, los nombres de los campos sern usados. Preferimos darle nombres nosotros mismos para
asegurarnos que stas sean capitalizadas apropiadamente e internacionalizadas.
Ahora, creamos un QSqlTableModel para visualizar el modelo. Ocultamos el campo id y establecemos
el ancho de la columna para acomodarse a su contenido sin la necesidad de mostrar puntos suspensivos.
El constructor de ArtistForm toma el nombre del artista que debe ser seleccionado cuando el dialogo
aparezca. Iteramos sobre los registros de la tabla artista y seleccionamos al artista especificado. El resto
del cdigo del constructor es usado para crear y conectar los botones y para ubicar los widgets hijos.
void ArtistForm::agregarArtista()
{
int fila = modelo->rowCount();
modelo->insertRow(fila);
QModelIndex indice = modelo->index(fila, Nombre_Artista);
tableView->setCurrentIndex(indice);
tableView->edit(indice);
}
Para agregar un nuevo artista, insertamos una sola fila vaca al fondo del QTableView. Ahora el usuario
puede ingresar un nuevo nombre de artista y un nuevo nombre de pas. Si el usuario confirma la insercin

147

13. Bases de Datos

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

148

13. Bases de Datos

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


para cubrir este caso.

Implementando Formularios Master-Detail


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

149

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

connect(cdTableView->selectionModel(),
SIGNAL(currentRowChanged(const QModelIndex &,
const QModelIndex &)),
this, SLOT(cdActualCambiado(const QModelIndex &)));
connect(cdModel, SIGNAL(beforeInsert(QSqlRecord &)),
this, SLOT(antesDeInsertarCd(QSqlRecord &)));
connect(trackModel, SIGNAL(beforeInsert(QSqlRecord &)),
this, SLOT(antesDeInsertarPista(QSqlRecord &)));
connect(trackModel, SIGNAL(rowsInserted(const QModelIndex &,

150

13. Bases de Datos

int,int)),
this, SLOT(refrescarCabeceraVistaPista()));

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

151

13. Bases de Datos

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

152

13. Bases de Datos

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

153

13. Bases de Datos

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

+= sql

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

154

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

Escribiendo Clientes FTP


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

155

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

156

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

157

14. Redes

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

158

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

como

un

QUrl

se

establece

usando

la

funcin

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

159

14. Redes

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

160

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 tambin debemos cerrar
cada archivo que fue abierto. Finalmente, llamamos a procesarSiguienteDir(). Si existe algn
direcotorio restante, el proceso entero comenzar nuevamente con el directorio que siga en la lista; de otra
manera, la descarga parar y la seal done() ser emitida.
Si no hay errores, la secuencia de comandos FTP y de la emisin de las seales ser de esta forma:
connectToHost(host, puerto)
login()
cd(directorio_1)
list()
emit listInfo(archivo_1_1)
get(archivo_1_1)
emit listInfo(archivo_1_2)
get(archivo_1_2)
...
emit done()
...
cd(directorio_N)
list()
emit listInfo(archivo_N_1)
get(archivo_N_1)
emit listInfo(archivo_N_2)
get(archivo_N_2)
...
emit done()
Si un archivo es, de hecho, un directorio, este ser agregado a la lista directoriosPendientes, y
cuando el ltimo archivo del comando list() ha sido descargado, se ejecuta un nuevo comando cd(),
seguido de un nuevo comando list() lista de comandos con el siguiente directorio pendiente, y todo el
proceso empieza de nuevo pero con el nuevo directorio. Esto se repite con los nuevos archivos descargados,
y con los nuevos directorios agregados a la lista directoriosPendientes, hasta que cada archivo haya
sido descargado de cada directorio, punto en el cual la lista directoriosPendientes estar vacia.
Si ocurre un error de red cuando se est descargando, por ejemplo, el quinto archivo de un total de veinte
archivos existentes en el directorio, entonces los archivos restantes no sern descargados. Si queremos
descargar tantos archivos como sea posible, una solucin podra ser programar las operaciones GET una a la
vez y esperar por la seal done(bool) antes de programar la siguiente. En listInfo(), simplemente
anexaramos el nombre del archivo a un QStringList, en lugar de llamar a get() en seguida, y en
donde va done(bool) llamariamos a get() con el siguiente archivo a descargar en el QStringList.
La secuencia de ejecucin sera como esta:
connectToHost(host, puerto)
login()

161

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

162

14. Redes

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

Escribiendo Clientes HTTP


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

163

14. Redes

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

164

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

Escribiendo Aplicaciones Clientes-Servidores TCP


Las clases QTcpSocket y QTcpServer pueden ser usadas para implementar clientes y servidores TCP.
TCP es un protocolo de transporte que forma la base de la mayora de los niveles de aplicacin de los
protocolos de internet, incluyendo FTP y HTTP, y puede ser usado tambin para crear protocolos propios.
TCP es un protocolo orientado a flujos (stream-oriented). Para las aplicaciones, los datos parecen ser un gran
flujo, y no un gran archivo plano. Los protocolos de alto nivel construidos sobre TCP son, generalmente,
tanto orientados a lneas como orientados a bloques.

Los protocolos orientados a lneas transfieren los datos como lneas de texto, cada una terminada
por una nueva lnea.

Los protocolos orientados a bloques tranfieren los datos como bloques de datos binarios. Cada
bloque consta de una campo tamao seguido de los datos en si.

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

165

14. Redes

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

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

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

166

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.
Tambin escondemos la barra de progreso, porque solo queremos que aparezca cuando una conexin es
activada. En Qt Designer, las propiedades minimun y mximum de la barra de progreso fueron establecidas
a cero. Esto le dice al QProgressBar que se comporte como un idicador de estado ocupado en lugar de
comportarse como una barra de progreso estndar basada en porcentajes.
En el constructor tambin conectamos las seales connected(), disconnected(),
readyRead() y error (QAbstract::SocketError) pertenceientes a QTcpSocket a distintos
slots privados.
void PlaneadorViaje::conectarAServidor()
{
tcpSocket.connectToHost("tripserver.zugbahn.de", 6178);
tableWidget->setRowCount(0);
botonBuscar->setEnabled(false);
botonParar->setEnabled(true);
labelEstado->setText(tr("Conectando con el servidor..."));
progressBar->show();
siguienteTamaoBloque = 0;
}
El slot conectarAServidor() es ejecutado cuando el usuario hace click en Buscar para empezar una
bsqueda. Llamamos a connectToHost() desde el objeto QTcpSocket para conectar con el servidor,
el cual asumimos es accesible por el puerto 6178 en el host ficticio tripserver.zugbahn.de (si
quieres intentar este ejemplo en tu propia mquina, reemplaza el nombre del servidor con
QHostAddress::LocalHost). El llamado a connectToHost() es asncrono; siempre retorna
inmediatamente. La conexin se establece, generalmente, un tiempo despus. El objeto QTcpSocket emite
la seal connected() cuando la conexin se establece y se encuentra activa., o emite la seal de error
(QAbstractSocket::SocketError) si la conexin falla.
Ahora, podemos actualizar la interfaz de usuario, haciendo la barra de progreso visible.

167

14. Redes

Finalmente, hacemos a la variable siguienteTamaoBloque igual a cero. Esta variable guarda el


tamao del siguiente bloque recibido desde el servidor. Hemos elegido usar el valor de 0 para indicar que no
sabemos todava el tamao del siguiente bloque.
void PlaneadorViaje::enviarSolicitud()
{
QByteArray blocque;
QdataStream salida(&bloque, QIODevice::WriteOnly);
salida.setVersion(QdataStream::Qt_4_1);
salida << quint16(0) << quint8(S) << fromComboBox->currentText()
<< haciaComboBox->currentText() << dateEdit->date()
<< timeEdit->time();
if (dePartidaRadioButton->isChecked()) {
salida << quint8(P);
} else {
salida << quint8(L);
}
salida.device()->seek(0);
salida << quint16(bloque.size() sizeof(quint16));
tcpSocket.write(bloque);
labelEstado->setText(tr(Enviando solicitud));
}
El slot enviarSolicitud() es ejecutado cuando el objeto QTcpSocket emite la seal
connected(), indicando que una conexin ha sido establecida. La tarea del slot es generar una solicitud al
servidor, con toda la informacin introducida por el usuario.
La solicitud es un bloque binario con el siguiente formato:
quint16

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

168

14. Redes

int fila = tableWidget->rowCount();


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

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

169

14. Redes

Y cmo funciona el ciclo forever? Si la variable siguienteTamaoBloque es 0, quiere decir que


no hemos ledo el tamao del siguiente bloque. Tratamos de leerlo (asumiendo que existen al menos 2 bytes
de espacio disponible para la lectura). El servidor usa un valor para el tamao de 0xFFFF para indicar que
no hay ms datos a ser recibidos, de manera que si leemos este valor, sabremos que hemos alcanzado el final.
Si el tamao del bloque de datos no es 0xFFFF, tratamos de leer el siguiente bloque. Primero, verificamos
para ver si existen bytes de tamao de bloque para ser leidos. Si no existe ninguno, paramos all mientras
tanto con el comando break. La seal readyRead() ser emitida nuevamente cuando estn disponibles
ms datos, y lo intentaremos de nuevo luego.
Una vez que estemos seguros de que un bloque completo ha llegado, podemos usar seguramente el operador
>> en el QDataStream para extraer la informacin relativa a un viaje, y creamos varios
QTableWidgetItem con esa informacin. Un bloque recibido desde el servidor posee el siguiente
formato:
quint16

Tamao 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 siguienteTamaoBloque a cero para indicar que el siguiente


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

170

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

171

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

172

14. Redes

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

173

14. Redes

canReadLine() y readLine() de la clase QTcpSocket en un slot conectado a la seal


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

Enviando y Recibiendo Datagramas UDP


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

174

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 medicin

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

175

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 inters aqu es udpSocket de tipo
QUdpSocket, la cual usaremos para recibir datagramas.
EstacionClimatologica:: EstacionClimatologica (QWidget *parent)
: QDialog(parent)
{
udpSocket.bind(5824);
connect(&udpSocket, SIGNAL(readyRead()),this,
SLOT(procesarDatagramasPendientes()));

Figura 14.3. La aplicacin Estacin Climatolgica

En el constructor, comenzamos con la vinculacin del QUdpSocket al puerto en el que el globo


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

176

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

177

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
propsitos generales que es popular en el intercambio y almacenamiento de datos. Qt provee dos APIs
diferentes para la lectura de documentos XML como parte de el modulo QtXml:

SAX (Simple API for XML/ API Sensilla para XML) reporta eventos de anlisis de texto
directamente a la aplicacin a travs de funciones virtuales.

DOM (Document Object Model/ Modelo de Objeto de Documento) convierte un documento XML
en tres estructuras, las cuales pueden ser navegadas por la aplicacin.

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

Leyendo XML con SAX


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

178

15. XML

El parseador llamara a los siguientes manejadores de eventos de parseo:


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

Aqu se est el archivo ndice que se muestra en el QTreeWidget de la Figura 15.2:


<?xml version="1.0"?>
<inidicelibro>
<entrada termino=espaciado">
<pagina>10</pagina>
<pagina>34-35</pagina>
<pagina>307-308</pagina>
</entrada>
<entrada termino=substraccion">
<entrada termino="de imagenes">
<pagina>115</pagina>
<pagina>244</pagina>
</entrada>
<entrada termino="de vectores">
<pagina>9</pagina>
</entrada>
</entrada>
</indicelibro>

179

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

180

15. XML

anidadora. Si la etiqueta no est anidada, entonces creamos el QTreeWidgetItem con el QTreeWidget


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

181

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

Leyendo XML con DOM


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

182

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 tambin nodos EntityReference, Text,
CDATASection, ProcessingInstruction y Comment. La Figura 15.3 muestra cules nodos
pueden tener cules tipos de nodos hijos. Los nodos en gris no pueden tener ningn hijo de su propio tipo.
Figura 15.3. Relacion padre-hijo entre nodos DOM

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

183

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

184

15. XML

En la funcin parsearEntrada(), creamos un tem QTreeWidget. Si la etiqueta est anidada dentro


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

Ecribiendo XML
Existen, bsicamente, dos mtodos para generar archivos XML desde una aplicacin Qt:

Podemos construir un rbol DOM y llamar al mtodo save() desde este.

Podemos generar el XML a mano.

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

185

15. XML

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

186

15. XML

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

187

16. Proporcionando Ayuda En Linea

16. Proporcionando Ayuda En Linea

Ayudas: Tooltips, Status Tips y Whats This?

Usando QTextBrowser Como un Mecanismo de Ayuda

Usando Qt Assistant como una Poderosa Ayuda En Linea

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

Ayudas: Tooltips, Status Tips, y Whats This?


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

188

16. Proporcionando Ayuda En Linea

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

Este es un ejemplo de un texto Whats This? aplicado a un dilogo:


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

189

16. Proporcionando Ayuda En Linea

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

Usando QTextBrowser como un Mecanismo de Ayuda


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

190

16. Proporcionando Ayuda En Linea

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

Aqu est el principio de la implementacin:


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

191

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

192

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 seccin de edicin.

Usando el Qt Assistant como una Poderosa Ayuda En Lnea


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

193

16. Proporcionando Ayuda En Linea

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

194

195

Parte III
Qt Avanzado

17. Internacionalizacin

17. Internacionalizacin

Trabajando con Unicode

Haciendo Aplicaciones que Acepten Traducciones

Cambio Dinmico del Lenguaje

Traduciendo Aplicaciones

Adicionalmente al alfabeto Latino usado para el ingls y para muchos otros lenguajes europeos, Qt 4 tambin
provee soporte para el resto de los sistemas de escritura del mundo:

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

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

196

17. Internacionalizacin

Trabajando con Unicode


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

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

197

17. Internacionalizacin

realizar con eficiencia estas operaciones, dado que ellas funcionarn para cualquier caracter Unicode. Las
funciones que provee QChar incluyen: isPrint(), isPunct(), isSpace(), isMark(),
isLetter(),
isNumber(),
isLetterOrNumber(),
isDigit(),
isSymbol(),
isLower() e isUpper(). Por ejemplo, aqu se muestra una manera de chequear si un caracter es un
dgito o una letra mayscula:
if(ch.isDigit()||ch.isUpper())

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

198

17. Internacionalizacin

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

));

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

);

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

199

17. Internacionalizacin

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

200

17. Internacionalizacin

Haciendo Aplicaciones que Acepten Traducciones


Si queremos hacer que nuestras aplicaciones estn disponibles en mltiples lenguajes, debemos hacer dos
cosas:

Asegurarnos de que cada cadena de texto visible para el usuario pase por la funcin tr().
Leer un archivo de traduccin (.qm) al iniciar la aplicacin.

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

201

17. Internacionalizacin

int main(int argc, char *argv[])


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

202

17. Internacionalizacin

for (int i = 0; flores[i]; ++i)


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

203

17. Internacionalizacin

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

204

17. Internacionalizacin

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

205

17. Internacionalizacin

Si la localidad del usuario es es (Espaol), :/italic.png se convierte en una referencia a la imagen


cursivo.png. Si la localidad es sv (Sueco), la imagen kursiv.png es usada. Para otras localidades, la
imagen italic.png ser usada.

Cambio Dinmico del Lenguaje


Para la mayora de las aplicaciones, detectar el lenguaje preferido por el usuario en el main() y cargar los
archivos .qm apropiados es un mtodo totalmente satisfactorio para sus fines. Pero existen algunas
situaciones donde el usuario puede necesitar la capacidad de poder cambiar el lenguaje de la aplicacin
dinmicamente. Una aplicacin que es usada continuamente por diferentes personas, puede necesitar que se
pueda cambiar el lenguaje sin tener que reiniciar la aplicacin. Por ejemplo, las aplicaciones usadas por los
operadores de los Centros de llamadas, las aplicaciones usadas por varios 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 aplicacin sin reiniciar.
Hacer una aplicacin capaz de cambiar el lenguaje dinmicamente requiere un poco ms de trabajo que
cuando se lee un nico archivo de traduccin al inicio de la aplicacin, pero no es difcil. Lo que se debe
hacer es:

Proporcionar un mtodo mediante el cual el usuario pueda cambiar el lenguaje.

Para cada widget o dilogo, colocar todas sus cadenas de texto traducibles en una funcin separada
(a menudo llamada retraducirUi()) y llamar a dicha funcin cuando el lenguaje sea cambiado.

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

206

17. Internacionalizacin

En el constructor, colocamos al widget central para que sea un objeto tipo JournalView, que es una
subclase de QTableWidget. Despus, colocamos unas cuantas variables miembros relacionadas a la
traduccin:

La variable Traductorapp es un objeto QTranslator usado para guardar las traducciones


actuales de la aplicacin.
La variable Traductorqt es un objeto QTranslator usado para guardar las traducciones de Qt.
La variable qmPath es un QString que especifica el path del directorio que contiene los archivos
de traduccin de la aplicacin.

Lo que hacemos es instalar dos objetos QTranslators en el QApplication: el objeto


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

207

17. Internacionalizacin

accionNuevo->setStatusTip(tr("Crear una publicacin nueva"));


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

208

17. Internacionalizacin

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

209

17. Internacionalizacin

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

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

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

2. Traducir la aplicacin usando Qt Linguist

210

17. Internacionalizacin

211

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

spreadsheet_de.ts \
spreadsheet_fr.ts

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

a los

archivos

spreadsheet_de.ts

Para ejecutar Qt Linguist en Windows, dirjase a Qt by Trolltech v4.x.y/Linguist. Para ejecutarlo en


distribuciones Linux, escriba linguist en la lnea de comandos. En Mac puede usar el Mac Os X Finder y

17. Internacionalizacin

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

212

17. Internacionalizacin

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

213

18. Multithreading

18. Multithreading

Creando Threads (hilos)

Sincronizando Threads

Comunicndose con el Thread Principal

Usando Clases Qt en Threads Secundarios

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

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

214

18. Multithreading

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

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

215

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 bsica.
DialogoHilo::DialogoHilo(QWidget *parent)
: QDialog(parent)
{
hiloA.establecerMensaje("A");
hiloB.establecerMensaje("B");
botonHiloA = new QPushButton(tr("Iniciar A"));
botonHiloB = new QPushButton(tr("Iniciar B"));
botonQuitar = new QPushButton(tr("Quitar"));
botonQuitar->setDefault(true);
connect(botonHiloA,SIGNAL(clicked()),this,
SLOT(iniciarODetenerHiloA()));
connect(botonHiloB, SIGNAL(clicked()),this,
SLOT(iniciarODetenerHiloB()));

}
En el constructor llamamos a establecerMensaje() para hacer que el primer hilo imprima
repetidamente el texto A y el segundo hilo imprima B.
void DialogoHilo::iniciarODetenerA()
{
if (HiloA.isRunning()) {
HiloA.detener();
botonHiloA->setText(tr("Iniciar A"));
} else {
HiloA.start();
botonHiloA->setText(tr("Detener A"));
}
}
Cuando el usuario haga click en el botn para el hilo A, la funcin iniciarODetenerHiloA() detiene
el hilo si ste estaba ejecutndose, sino estaba ejecutndose entonces lo inicia. Esta funcin tambin actualiza
el texto del botn.
void DialogoHilo::iniciarODetenerHiloB()
{
if (hiloB.isRunning()) {
hiloB.detener();
botonHiloB->setText(tr("Iniciar B"));
} else {

216

18. Multithreading

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

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

QMutex mutex;
};
La funcin run() se cambiara por esta:
void Hilo::run()
{
porsiempre {
mutex.lock();
if (detenido) {
detenido = false;
mutex.unlock();
break;
}

217

18. Multithreading

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

218

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 generalizacin de los mutex, pero a distincin de los bloqueadores de
lectura/escritura, los semforos (tipo de dato QSemaphore) pueden ser usados para salvaguardar cierto
nmero de recursos idnticos. Los siguientes dos segmentos de cdigo muestran la correspondencia entre
QSemaphore y QMutex:
QSemaphore semaforo(1);
semaforo.acquire();
semaforo.release();

QMutex mutex;
mutex.lock();
mutex.unlock();

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

La necesidad de sincronizar en el ejemplo de productor-consumidor es doble: si el productor genera los datos


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

219

18. Multithreading

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

220

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

221

18. Multithreading

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

222

18. Multithreading

Comunicndose con el Thread Principal


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

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

223

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 seales emitidas por el objeto HiloTransaccion, el cual revisaremos en un momento.
void VentanaImagen::voltearHorizontalmente()
{
agregarTransaccion(new TransaccionVoltear(Qt::Horizontal));
}
El slot voltearHorizontalemente() crea una transaccin de rotacion y la registra usando la
funcin
privada
agregarTransaccion().
Las
funciones
voltearVerticalmente,
redimensionarImagen(), convertirA32Bit(), convertirA8Bit() y convertirA1Bit
son similares.
void VentanaImagen::agregarTransaccion(Transaccion *transac)
{
thread.agregarTransaccion(transac);
accionAbrir->setEnabled(false);
accionGuardar->setEnabled(false);
accionGuardarComo->setEnabled(false);
}
La funcin agregarTransaccion() agrega una transaccin a la lnea de transacciones del hilo
secundario y deshabilita las acciones Abrir, Guardar y Guardar Como mientras las transacciones estn siendo
procesadas.
void VentanaImagen::todasTransaccionesHechas()
{
accionAbrir->setEnabled(true);
accionGuardar->setEnabled(true);
accionGuardarComo->setEnabled(true);
imagenLabel->setPixmap(QPixmap::fromImage
(thread.imagen()));
setWindowModified(true);
statusBar()->showMessage(tr("Listo"), 2000);
}
El slot todasTransaccionesHechas() es llamado cuando la lnea de transacciones de
TransacctionThread queda vaca.
Ahora, vamos a ver la clase HiloTransaccion:
class HiloTransaccion : public QThread
{

224

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

225

18. Multithreading

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

226

18. Multithreading

return QObject::tr("Volteando imagen


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

Usando Clases Qt en Threads Secundarios


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

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

Debemos eliminar todos los QObjects creados en un hilo secundario antes


de eliminar el objeto QThread correspondiente.
Esto puede hacerse creando objetos en la pila; o stack, en QThread::run().

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


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

227

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

228

19. Creando Plugins

19. Creando Plugins

Extendiendo Qt con Plugins

Haciendo Aplicaciones que Acepten Plugins

Escribiendo Plugins para Aplicaciones

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

Extendiendo Qt con Plugins


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

229

19. Creando Plugins

Figura 19.1 Plugin Qt y clases manejadoras

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

230

19. Creando Plugins

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

231

19. Creando Plugins

mutable int numImagenes;


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

232

19. Creando Plugins

Figura 19.2 Formato de archivo .cur

La siguiente funcin es muy compleja, as que vamos a revisarla por partes:


bool ManejadorCursor::leer(QImage *imagen)
{
LeerCabeceraSiEsNecesario();
if (estado != ImagenAnterior)
return false;
La funcin leer() lee los datos para cualquier imagen que comience en la posicin actual del puntero al
objeto (device). Si la cabecera del archivo es leda satisfactoriamente, o despus de que una imagen haya
sido leda y el puntero al objeto (device) est en el comienzo de otra imagen, podemos leer la siguiente
imagen.
quint32 tamao;
quint32 ancho;
quint32 alto;
quint16 numPlanos;
quint16 bitsPorPixel;
quint32 compresion;
QDataStream in(device());
in.setByteOrder(QDataStream::LittleEndian);
in >> tamao;
if (size != 40) {
entrarEstadoError();
return false;
}
in >> ancho >> alto >> numPlanos >> bitsPorPixel >>
alto /= 2;

compresion;

if (numPlanos != 1 || bitsPorPixel != 1 ||
compresion != 0) {
entrarEstadoError();
return false;
}
in.skipRawData((tamao - 20) + 8);
Creamos un QDataStream para leer el objeto (device). Debemos establecer el orden de bytes para
encontrar el especificado por las especificaciones del formato de archivo .cur. No es necesario establecer
un numero de versin para un QDataStream ya que el formato de nmeros enteros y de punto flotante no
varan entre las versiones de data stream. A continuacin, leemos en varios tems de datos de cabecera del

233

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

234

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

235

19. Creando Plugins

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

236

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

Haciendo Aplicaciones que Acepten Plugins


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

237

19. Creando Plugins

Figura 19.3 La aplicacin Text Art

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

238

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

239

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 funcin llenarListWidget() comienza con la creacin de algunas variables para pasarlas a la
funcin aplicarEfecto(), en particular una fuente, una pluma, y un gradiente lineal. Luego se itera
sobre cada InterfazTextArt que sea encontrado por cargarPlugins(). Para cada efecto
proporcionado por cada interfaz, se crear un QListWidgetItem nuevo con su texto establecido con el
nombre del efecto, y con un QPixmap creado usando aplicarEfecto().
En esta seccin, hemos visto cmo cargar plugins llamando a cargarPlugins() en el constructor, y
cmo hacer uso de ellos en llenarListWidget(). El cdigo sale airoso si no hay plugins
proporcionando objetos tipo InterfazTextArt, si se proporciona uno, o ms de uno. Adems, los
plugins adicionales podran ser agregados luego: Cada vez que la aplicacin comience, debe cargar cuantos
plugin encuentre que proporcionen las interfaces que la aplicacin quiera. Esto facilita la extensin de la
funcionalidad de la aplicacin sin cambiar la aplicacin en s misma.

Escribiendo Plugins para Aplicaciones


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

240

19. Creando Plugins

QStringList PluginEfectosBasicos::efectos() const


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

241

19. Creando Plugins

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

242

20. Caractersticas Especficas de Plataformas

20. Caractersticas Especficas de Plataformas

Haciendo Interfaces con APIs Nativas

Usando Activex en Windows

Manejando la Administracin de Sesin en X11

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

http://www.trolltech.com/products/solutions/catalog/
http://www.froglogic.com/tq/
http://www.kdab.net/knut/

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

Creando Interfaces con APIs Nativas


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

243

20. Caractersticas Especficas 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 cmo modificaramos una propiedad de ventana:


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

244

20. Caractersticas Especficas de Plataformas

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

245

20. Caractersticas Especficas 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 grfico X11 en un QPixmap o QWidget, por
ejemplo.
Las aplicaciones Qt que necesiten en su interfaz otros kits de herramientas o libreras necesitan muy
frecuentemente acceder a los eventos de bajo nivel (XEvents en X11, MSGs en Windows, EventRef en
Mac OS X, QWSEventes en Qtopia) antes de ellos sean convertidos en QEvents. Podemos hacer esto
haciendo una subclase de QApplication y re implementar los filtros de eventos especficos de plataforma
ms relevantes, uno de x11EventFilter(), winEventFilter(), macEventFilter() y
qwsEventFilter(). Alternativamente, podemos acceder a los eventos especficos de plataforma que son
enviados a un QWidget dado por medio de la re implementacin de x11Event(), winEvent(), y
qwsEvent(). Esto puede ser muy til para manejar ciertos tipos de eventos que Qt normalmente ignora,
como los eventos de joystick.
Para ms informacin acerca de los asuntos especficos de plataformas, incluyendo cmo desarrollar
aplicaciones Qt en diferentes plataformas, vea http://doc.trolltech.com/4.1/winsystem.html.

246

20. Caractersticas Especficas de Plataformas

Usando Activex en Windows


La tecnologa ActiveX de Microsoft permite a las aplicaciones incorporar componentes de interfaz de
usuario proporcionados por otras aplicaciones o libreras. Esta construido en Microsoft COM y define un
conjunto de interfaces para las aplicaciones que usen componentes y otro conjunto de interfaces para
aplicaciones y libreras que proveen componentes.
La Edicin de Escritorio (Qt/Windows Desktop Edition) Qt/Windows proporciona el framework ActiveQt
para combinar limpiamente ActiveX y Qt. ActiveQt consta de dos mdulos:

El mdulo QAxContainer nos permite usar objetos COM e incrustar embebidamente controles
ActiveX en aplicaciones Qt.
El mdulo QAxServer nos permite exportar objetos COM personalizados y controles AciveX
escritos usando Qt.

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

La ventana principal de la aplicacin es de tipo PlayerWindow:


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

247

20. Caractersticas Especficas 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 seccin de datos privados, declaramos un dato
miembro QAxWidget *:
PlayerWindow::PlayerWindow()
{
wmp = new QAxWidget;
wmp->setControl("{22D6F312-B0F6-11D0-94AB-0080C74C7E95}");
En el constructor, comenzamos con la creacin de un objeto QAxWidget para encapsular el control
ActiveX Windows Media Player. El modulo QAxContainer consta de tres clases: QAxObject que
encapsula un objeto COM, QAxWidget que encapsula un control ActiveX y QAxBase que implementa el
ncleo de funcionalidad COM para QAxObject y QAxWidget.
Llamamos a setControl() en el objeto QAxWidget (wmp) con el ID de la clase del control de
Windows Media Player 6.4. Esto creara una instancia del componente requerido. A partir de all, todas las
propiedades, eventos y mtodos del control ActiveX estn disponibles como propiedades Qt, signals y slots
mediante el objeto QAxWidget.
Figura 20.4 rbol de herencia del mdulo QAxContainer

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

248

20. Caractersticas Especficas 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 Relacin entre tipos COM y tipos Qt

Despus del llamado a QAxWidget::setControl(), llamamos a QObject::setProperty() para


establecer la propiedad ShowControls del Reproductor Windows Media a false, puesto que
proveeremos nuestros propios botones para manipular el componente. QObject::setProperty()
puede ser usado tanto para propiedades COM como para propiedades Qt normales. Su segundo parmetro es
un tipo QVariant.
Ahora, llamamos a setSizePolicy() para hacer que el control ActiveX tome todo el espacio disponible
en el layout, y conectamos tres eventos ActiveX del componente COM a tres slots.

stopButton = new QToolButton;


stopButton->setText(tr("&Stop"));
stopButton->setEnabled(false);
connect(stopButton, SIGNAL(clicked()), wmp, SLOT(Stop()));

}
El resto del constructor de PlayerWindow sigue el patrn usual, excepto que conectamos algunas seales
Qt a slots proporcionados por el objeto COM (Play(), Pause() y Stop()). Como los botones son
similares, nosotros hemos mostrado aqu solamente la implementacin del botn Stop.
Dejemos al constructor y veamos la funcin timerEvent():

249

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

250

20. Caractersticas Especficas 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 estn
implementadas por QAxServer.
Cuando hacemos herencia mltiple incluyendo una clase derivada de QObject, debemos poner siempre
primero la clase derivada de QObject de manera que moc pueda recogerla.
Figura 20.6 El widget AxBouncer en Internet Explorer

Declaramos tres propiedades de lectura-escritura y una propiedad de solo-lectura. El macro Q_ENUMS() es


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

251

20. Caractersticas Especficas de Plataformas

El constructor de AxBouncer es un constructor estndar para un widget, con parmetro parent. El


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

252

20. Caractersticas Especficas de Plataformas

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

253

20. Caractersticas Especficas 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 seccin mostrara como exportar
muchos controles ActiveX.
El primer argumento de QAXFACTORY_DEFAULT() es el nombre de la clase Qt a exportar. Este es
tambin el nombre bajo el cual el control es exportado. Los otros cinco argumentos son el ID de la clase, el
ID de la interfaz, el ID del evento de interfaz, el ID del tipo de librera y el ID de la aplicacin. Podemos usar
herramientas estndares como guidgen o uuidgen para generar estos identificadores. Como el server es
una librera, no necesitamos una funcin main().
Aqu est el archivo .pro para nuestro servidor ActiveX en-proceso:
TEMPLATE
CONFIG
HEADERS
SOURCES
RC_FILE
DEF_FILE

= lib
+= dll qaxserver
= axbouncer.h \
objectsafetyimpl.h
= axbouncer.cpp \
main.cpp \
objectsafetyimpl.cpp
= qaxserver.rc
= qaxserver.def

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

254

20. Caractersticas Especficas 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 aplicacin de Libro de Direcciones (Address Book) escriptable. La
aplicacin puede servir como una aplicacin Qt/Windows estndar o un servidor ActiveX fuera-de-proceso.
La otra posibilidad nos permite escriptar la aplicacin usando, por ejemplo, Visual Basic.
class AddressBook : public QMainWindow
{
Q_OBJECT
Q_PROPERTY(int count READ count)
Q_CLASSINFO("ClassID", "{588141ef-110d-4beb-95abee6a478b576d}")
Q_CLASSINFO("InterfaceID", "{718780ec-b30c-4d88-83b379b3d9e78502}")
Q_CLASSINFO("ToSuperClass", "AddressBook")
public:
AddressBook(QWidget *parent = 0);
~AddressBook();
int count() const;
public slots:
ABItem *createEntry(const QString &contact);
ABItem *findEntry(const QString &contact) const;
ABItem *entryAt(int index) const;
private slots:
void addEntry();
void editEntry();
void deleteEntry();
private:
void createActions();
void createMenus();
QTreeWidget *treeWidget;
QMenu *fileMenu;
QMenu *editMenu;
QAction *exitAction;
QAction *addEntryAction;
QAction *editEntryAction;
QAction *deleteEntryAction;
};
El widget AddressBook es la ventana principal de la aplicacin. La propiedad y los slots que este provee
estarn disponibles para escripting. El macro Q_CLASSINFO() es usado para especificar los ID de la clase
y de la interfaz asociadas con la clase. Estas fueron generadas usando una herramienta tal como guid o
uuid.
En el ejemplo anterior, especificamos los ID de la clase y de la interfaz cuando exportamos la clase
QAxBouncer usando el macro QAXFACTORY_DEFAULT(). En este ejemplo, queremos exportar muchas
clases, de manera que no podremos usar el macro QAXFACTORY_DEFAULT(). Existen dos opciones
disponibles para nosotros:

255

20. Caractersticas Especficas de Plataformas

Podemos hacer subclases de QAxFactory, re implementando sus funciones virtuales para


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

De vuelta a la definicin de la clase AddressBook: la tercera ocurrencia de Q_CLASSINFO() puede


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

256

20. Caractersticas Especficas de Plataformas

aplicacin corra como un servidor. Si la aplicacin no corre como un servidor, creamos el widget principal y
lo mostramos como lo haramos normalmente en una aplicacin stand-alone de Qt.
Adicionalmente a activex, los servidores ActiveX entienden las siguientes opciones de lnea de
comandos:

-regserver registra el servidor en el registro del sistema.


-unregserver quita del registro del sistema al servidor.
-dumpidlfile escribe el IDL del servidor al archivo especificado.

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

FORMS
RC_FILE

= app
+= qaxserver
= abitem.h \
addressbook.h \
editdialog.h
= abitem.cpp \
addressbook.cpp \
editdialog.cpp \
main.cpp
= editdialog.ui
= qaxserver.rc

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

Manejando la Administracin de Sesin en X11


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

257

20. Caractersticas Especficas de Plataformas

El componente especifico de X11 que se encarga de guardar y restaurar la sesin es llamado el


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

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

258

20. Caractersticas Especficas de Plataformas

Figura 20.8 La aplicacin Tic-Tac-Toe

Aqu est la definicin de la clase Application:


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

259

20. Caractersticas Especficas de Plataformas

appname -session id_key


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

260

20. Caractersticas Especficas 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 mtodos sizeHint(), paintEvent(),
y mousePressEvent(). Este tambin proporciona las funciones gameInProgress() y
saveState() que usamos en nuestra clase Application.
TicTacToe::TicTacToe(QWidget *parent)
: QWidget(parent)
{
clearBoard();
if (qApp->isSessionRestored())
restoreState();
setWindowTitle(tr("Tic-Tac-Toe"));
}
En el constructor, limpiamos el tablero, y si la aplicacin fue invocada con la opcin session, llamamos
a la funcin privada restoreState() para recargar la sesin antigua.

261

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

262

20. Caractersticas Especficas de Plataformas

En restoreState(), leemos el archivo que corresponde a la sesin restaurada y rellenamos el tablero


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

263

21. Programacin Embebida

21. Programacin Embebida

Iniciando con Qtopia

Personalizando Qtopia Core

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

Iniciando con Qtopia


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

264

21. Programacin Embebida

El sistema de configuracin de Qtopia Core soporta compiladores cruzados, a travs de configure y de la


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

265

21. Programacin Embebida

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

Personalizando Qtopia Core


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

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

266

21. Programacin Embebida

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

267

Glosario

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

Mutex (Mutual Exclusin)


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

268

También podría gustarte