sábado, 22 de agosto de 2009

Qt4, QComboBox y el foco del teclado

Esta vez vamos a escribir un post que será útil para desarrolladores de la libreria Qt (version 4) que tengan interes en profundizar en la gestión del foco de teclado del widget QComboBox. Vamos a modificar el comportamiento standard de este widget, y supongo que por ese motivo la documentación de la libreria es poco clara en este sentido. Por ello creo que este post os puede servir para entender mejor este asunto.


La idea es implementar un widget custom haciendo uso de dos de los widgets predefinidos por la libreria: QLabel y QComboBox. Nuestra widget custom (ComboLabel) compondrá el QLabel y el QComboBox en un solo widget, de forma que cuando reciba el foco del teclado, desplegará automáticamente la lista del QComboBox, y cuando pierda el foco, la esconderá automaticamente. Vamos a usar la tecla "Enter" (Qt::Key_Enter) para hacer la transición del foco.

Este problema no plantea complicaciones en entornos donde existe un raton o una pantalla tactil, ya que en estos casos, el puntero aisla de los problemas de foco. Sin embargo, en los sistemas donde el único dispositivo de entrada es un teclado, la gestion del foco se convierte en uno de los problemas clave a gestionar y resolver por el desarrollador. Bueno, en uno mas :-)

Os planteo una primera propuesta para resolver el problema que en principio debe parecer obvia (aunque veremos que no funciona). Primero crear una subclase de QWidget (ComboLabel) que componga una instancia de la clase QLabel y de QComboBox en un mismo layout. Despues reimplementar los filtros de entrada (focusInEvent) y salida (focusOutEvent) del foco, para mostrar y ocultar automáticamente la lista. Por ultimo reimplementar el filtro de eventos de teclado (keyPressEvent) para hacer la transicion del foco cuando el usuario pulse la tecla Qt::Key_Enter.

ComboLabel::ComboLabel(QString title, QWidget *parent) : QWidget(parent)
{
setFocusPolicy(Qt::StrongFocus);

m_label = new QLabel(title);
m_combo = new QComboBox;

QHBoxLayout * mainLayout = new QHBoxLayout;
mainLayout->addWidget(m_label);
mainLayout->addWidget(m_combo);
setLayout(mainLayout);
}

void ComboLabel::focusInEvent(QFocusEvent * event)
{
m_combo->showPopup();
}

void ComboLabel::focusOutEvent(QFocusEvent * event)
{
m_combo->hidePopup();
}

void ComboLabel::keyPressEvent(QKeyEvent * event)
{
switch(event->key()) {
case Qt::Key_Enter:
focusNextChild();
break;
default:
QWidget::keyPressEvent(event);
}
}

Esta propuesta parece obvia pero no funciona como queremos. El problema viene dado por la implementación del QComboBox, ya que este widget esta compuesto internamente por dos widgets, un QLineEdit y un QAbstractItemView, y cuando el QAbstractItemView del QComboBox toma el foco del teclado, nunca mas lo devuelve al padre. ¿Como resolvemos entonces este asunto?

La solución del problema (bastante tricky como vereis) consiste en crear una clase ComboBox (derivada de QComboBox), e instalar un filtro de eventos (installEventFilter()) en la vista desplegada (view()). Con este filtro se detectan los eventos de pulsacion de tecla sobre el QAbstractItemView, y se hace la transicion del foco cuando el usuario pulsa sobre dicha vista la tecla Qt::Key_Enter. Para terminar, cuando el widget recibe el foco debe desplegar la vista (showPopup()) y cuando abandona el foco, debe esconderla (hidePopup()). Ambos métodos deben ser reimplementados tal y como veremos, ya que la implementación de la clase padre (QComboBox) no es suficiente para nuestro objetivo.


Veamos los detalles:

ComboLabel::ComboLabel(QString title, QWidget *parent) : QWidget(parent)
{
m_label = new QLabel(title);
m_combo = new ComboBox;

QHBoxLayout * mainLayout = new QHBoxLayout;
mainLayout->addWidget(m_label);
mainLayout->addWidget(m_combo);
setLayout(mainLayout);

setFocusProxy(m_combo);
}

ComboBox::ComboBox(QWidget * parent) : QComboBox(parent)
{
view()->installEventFilter(this);
}

bool ComboBox::eventFilter(QObject * obj, QEvent * event)
{
if (obj == view() && event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast(event);
switch (keyEvent->key()) {
case Qt::Key_Enter:
hidePopup();
focusNextChild();
return QComboBox::eventFilter(obj, event);
}
}
return QObject::eventFilter(obj, event);
}

void ComboBox::showPopup()
{
if (!(view()->isVisible())) {
setRootModelIndex(QModelIndex());
QComboBox::showPopup();
}
}

void ComboBox::hidePopup()
{
if (view()->isVisible()) {
setRootModelIndex(view()->currentIndex().parent());
setCurrentIndex(view()->currentIndex().row());
QComboBox::hidePopup();
}
}

void ComboBox::focusInEvent(QFocusEvent * event)
{
if (count() > 0) showPopup(); else focusNextChild();
}

Para que podais probar el ejemplo en vuestro ordenador y analizarlo con mas calma, os dejo este tarball aqui. Insistid porque parece que el servidor web también esta de vacaciones :-). En él encontrareis todos los fuentes listos para ser compilados.

Espero que este pequeño ejemplo os sirva en el futuro para resolver mejor vuestros problemas de foco con QComboBox'es y toda clase de widgets custom que implementeis de ahora en adelante. La librería Qt4 es una librería muy extensa, asi que si alguien tiene interés en tratar algun tema en particular, podeis mandarme un correo sin ningun compromiso e intentaré hacer lo que pueda para ayudaros.

Por cierto... ¿que tal lo habeis pasado estas vacaciones? Las mías cortitas, este verano se han terminado pronto. Y terminadas las vacaciones, como veis, volvemos con energias renovadas y mas ganas de transmitir nuevos conocimientos a los lectores de este blog. Estamos en epoca de crisis, pero ilusion que no falte, que mientras nadie diga lo contrario... ¡sigue siendo gratis! :-)

sábado, 1 de agosto de 2009

Organizar un proyecto en C con librerias

Los programadores newbies en Linux podeis aprovechar este post para aprender a compilar y a usar vuestras librerías en C. Encontrareis muchos manuales en Internet mucho mas extensos y elaborados que este, pero yo lo que quiero transmitiros son conocimientos de forma rapida, para que lo pilleis a la primera sin necesidad de que perdais mucho tiempo. Así que, sin anestesia, nos ponemos manos a la obra:



Información de background

Segun programamos, encontramos funciones que se usan en muchas partes de nuestro codigo. Nos interesa tenerlas en un directorio separado, compiladas y listas para poder usarlas siempre que queramos. Las ventajas de esto son enormes:
- Evitamos los continuos copy/paste (código mas compacto)
- Reducimos tiempo de compilacion (solo se compilan una vez)
- Aumentamos fiabilidad con el tiempo de uso (mas probadas, menos bugs)

La librería estara compuesta por uno o mas ficheros .c fuentes y uno o mas ficheros .h de cabeceras. Veamos un ejemplo:
$ cat milib.h
#ifndef _MI_LIB_H
#define _MI_LIB_H
int suma(int a, int b);
int resta(int a, int b);
#endif

$ cat milib.c
int suma(int a, int b) { return a+b; }
int resta(int a, int b) { return a-b; }


Librerías estáticas vs librerías dinámicas

En linux podemos hacer dos tipos de librerías:
- Librerias estáticas
- Librerias dinámicas

Las librerías estaticas se añaden a la imagen de nuestro ejecutable. Una vez hecha esta operación, la librería puede borrarse y el código sigue funcionando, ya que nuestro binario resultante contiene todo lo que necesita de la librería.

Las librerías dinámicas no se añaden a la imagen de nuestro ejecutable. El ejecutable debe buscar la librería siempre que la necesite, y por tanto, la librería dinámica no puede borrarse, o nuestro programa dejaría de funcionar.

La elección del tipo de librería que debeis usar depende de la naturaleza del proyecto desarrollado. Es una eleccion en la que debeis valorar ventajas e inconvenientes, y llegar a un compromiso entre ambas:
- Ejecutable estatico ocupa mas espacio
- Ejecutable estatico es mas facil de instalar en otro PC
- Ejecutable estatico es mas rapido
- Ejecutable dinamico aprovecha mejoras de la librería compatible con el código del programa
- Ejecutable dinamico requiere refactorización con cambios incompatibles

En Linux (en general Unix) las librerías estáticas se nombran con la extension .a (libmia.a) y las librerías dinámicas se nombran con la extension .so (libmia.so).

Receta para construir y usar una librería estática

Partimos de nuestro programa miprograma.c y de los fuentes de nuestra libreria milib.c.
$ cat miprograma.c
#include
int main(void) {
printf("%d\n", suma(10,10));
printf("%d\n", resta(10,10));
}


Veamos los pasos necesarios para construir y hacer uso de una librería estática:
- Obtener los ficheros objeto .o de todos los fuentes .c de nuestra libreria
$ gcc -c milib.c -o milib.o

- Crear la libreria .a
$ ar -rv libmia.a milib.o

- Compilar nuestro programa, linkando con la libreria estatica
$ gcc -o miprograma miprograma.c -I. -L. -Bstatic -lmia

Observar que -Bstatic -lmia indica que se debe coger la libreria libmia.a. El prefijo lib y la extension .a ya las pone automaticamente el compilador gcc. La opción -Bstatic afecta a todas las librerías que van detrán en la línea de compilación.

Receta para compilar y usar una librería dinámica

Partimos de nuestro programa miprograma.c y de los fuentes de nuestra libreria milib.c. Veamos los pasos necesarios para construir y hacer uso de una librería dinámica:
- Obtener los ficheros objeto .o de todos los fuentes .c de nuestra libreria
$ gcc -c milib.c -o milib.o

- Crear la libreria .so
$ ld -o libmia.so milib.o -shared

- Compilar nuestro programa, linkando con la libreria dinámica
$ gcc -o miprograma miprograma.c -I. -L. -Bdynamic -lmia

Observar que -Bdynamic -lmia indica que se debe coger la libreria libmia.so El prefijo lib y la extension .so ya las pone automaticamente el compilador gcc. La opción -Bdynamic afecta a todas las librerías que van detrán en la línea de compilación.

El comando ldd sirve para ver las librerias dinamicas con las que se ha linkado un programa. Por ejemplo:
$ ldd miprograma
linux-gate.so.1 => (0xbfffe000)
libmia.so => not found
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7e6f000)
/lib/ld-linux.so.2 (0xb7faa000)

Como vemos en el ejemplo, el programa no encuentra la libreria libmia.so. Hay que decirle al programa donde estan las librerías dinámicas. Para ello definimos la variable de entorno LD_LIBRARY_PATH de esta manera:
$ export LD_LIBRARY_PATH=/path/a/miprograma
$ ldd miprograma
linux-gate.so.1 => (0xbfffe000)
libmia.so => /path/a/miprograma/libmia.so (0xb7f2f000)
libc.so.6 => /lib/tls/i686/cmov/libc.so.6 (0xb7df7000)
/lib/ld-linux.so.2 (0xb7f34000)


Cierre del post

Es hora de que practiqueis vosotros, os tomeis un tiempo para probar este ejemplo y asimilar los conceptos, y empeceis a aplicar estos conocimientos en vuestros futuros proyectos.

Yo me despido del blog hasta despues del verano. Hoy cumplo 33 años y quiero aprovechar la ocasión para dedicarle este post a todos mis lectores, y en especial a aquellos que se acuerden de felicitarme. Os agradezco a todos las felicitaciones, pero yo lo que quiero son regalos little bit frikies! ;-) Que paseis un buen verano y nos leemos en Septiembre! :-)

Visitas:

Seguidores