miércoles, 25 de enero de 2012

El mecanismo de conexiones, señales y slots en Qt

Este post asume que el lector tiene ciertos conocimientos en programación C++ y en la librería Qt. Si no sabes de que te estoy hablando, es mejor que dediques tu tiempo a leer otro sitio, porque te vas a aburrir de lo lindo. Avisado quedas.



=== La base teórica ===

El mecanismo de señales y slots es una de las características mas importantes de la librería Qt, probablemente la que le diferencia de otros framework para desarrollar interfaces de usuario.

Las señales y los slots se usan para las comunicaciones entre los objetos del interfaz. En general, la señal (signal) parte de un objeto "emisor" y llega a un objeto "receptor". El objeto receptor decide si ejecuta un slot y finaliza el proceso, o emite una nueva señal que propaga el evento hacia otro objeto receptor. El proceso se puede repetir.

Como ejemplo sencillo, imagina un botón de "Apagar". Cuando el usuario lo pulsa, el boton genera la señal clicked(). Y en respuesta a esa señal, el sistema ejecuta un slot que hace un apagado ordenado de todo el sistema.

Para establecer una conexión entre la señal emitida por un objeto "emisor" y un slot que ejecuta el objeto "receptor", se usa el metodo connect() y las macros SIGNAL() y SLOT(). La sintaxis es esta:

    connect(emisor, SIGNAL(signal_emitida()), receptor, SLOT(slot_doaction()));


Cuando el widget "emisor" genera la señal "signal_emitida", el widget "receptor" ejecuta el código del slot "slot_doaction". La llamada connect, y las macros SIGNAL() y SLOT() forman parte de la sintaxis de Qt, y no son parte del estandar C++. Para compilarlas, se usa un meta-object compiler (moc) que traduce estas macros en C++ estándar. Esta parte escapa del propósito de este post , aunque si sientes interés, no dudes en preguntarme.

Algunas consideraciones a tener en cuenta:

* Una misma señal puede conectarse a distintos slots
* Distintas señales pueden conectarse a un mismo slot
* Una señal puede conectarse a otra señal, lo que emite una segunda signal en el widget receptor inmediatamente después de recibir la primera.

La lista de parámetros de la señal signal_emitida(signature) debe coincidir con la lista de parámetros del slot_doaction(signature). Pero el slot slot_doaction(signature) puede tener menos parámetros que los que tiene la señal signal_emitida(signature), en cuyo caso los parámetros adicionales son simplemente ignorados. Aunque parezca un poco extraño, fíjate que tiene sentido, ya que Qt es capaz de ignorar argumentos sobrantes, pero en ningún caso puede inventar argumentos de la nada. Algunos ejemplos para aclarar este punto:

                   Signals      Slots                   ¿isOK?
rangeChanged(int, int) setRange(int, int) OK
valueChanged(int) setValue() OK
clicked() setValue(int) NOK


Todas las clases que heredan de QObject (o alguna de sus subclases como QWidget), pueden contener señales, slots y conexiones. Pero para que sea posible definirlas, es necesario que la clase mencione la macro Q_OBJECT al comienzo de su declaración.


=== Sintaxis en la práctica ===

Veamos un ejemplo de como se realiza una conexión. Para ello, necesitamos dos clases ("Emisor" y "Receptor") que sean agregaciones de una clase contenedora W (dos partes componentes, o si lo prefieres, dos variables de W). Para enviar un mensaje desde "Emisor" hasta "Receptor" haríamos la siguiente conexión en W (pongo pseudocódigo para que te quedes con la idea):

    class W: public QObject
{
Q_OBJECT
...
Emisor e;
Receptor r;
connect(e, SIGNAL(signalChangePos()), r, SLOT(slotChangePosition()));
};

class Emisor: public QObject
{
Q_OBJECT
...
signal signalChangePos();
...
emit signalChangePos();
};

class Receptor: public QObject
{
Q_OBJECT
...
slot slotChangePosition();
};


Veamos ahora la declaración de una clase MiWidget que maneja señales y slots. MiWidget hereda de QWidget, y a su vez QWidget hereda de QObject:

    class MiWidget : public QWidget
{
Q_OBJECT // Macro es necesaria cuando la clase define sus propias señales o slots

public:
MiWidget(QWidget *parent = 0);
void foo(QString &text);

signals: // señales emitidas por esta clase
void findnext(const QString &str);
void findprev(const QString &str);

private slots: // slots de esta clase
void enableFindButton(const QString &text);
};


Supongamos que el metodo foo emite una signal, veamos como se realiza la implementación:

    void MiWidget::foo(QString &text)
{
emit findprev(text); // se emite la señal findprev con el texto "text"
}



=== Un ejemplo práctico ===

Un escenario común ocurre cuando quieres pasar valores constantes en la sentencia connect. Esto ocurre por ejemplo cuando quieres implementar un teclado QWERTY usando QPushButtons como teclas. Podrías pensar que lo lógico es implementar algo como esto:

    connect(key_q, SIGNAL(pressed()), panelTexto, SLOT(keyPressed('q')));
connect(key_w, SIGNAL(pressed()), panelTexto, SLOT(keyPressed('w')));
connect(key_e, SIGNAL(pressed()), panelTexto, SLOT(keyPressed('e')));
connect(key_r, SIGNAL(pressed()), panelTexto, SLOT(keyPressed('r')));
connect(key_t, SIGNAL(pressed()), panelTexto, SLOT(keyPressed('t')));
...


Pero esto no es valido en Qt, y no te funcionaría. Para implementar un teclado, la opción fácil sería usar un montón de QPushButton, y asignar a cada uno un slot diferente. La implementación sería como esto:

    connect(key_q, SIGNAL(pressed()), panelTexto, SLOT(press_q()));
connect(key_w, SIGNAL(pressed()), panelTexto, SLOT(press_w()));
connect(key_e, SIGNAL(pressed()), panelTexto, SLOT(press_e()));
connect(key_r, SIGNAL(pressed()), panelTexto, SLOT(press_r()));
connect(key_t, SIGNAL(pressed()), panelTexto, SLOT(press_t()));
...


Y la lista de slots:

    private slots:
void press_q();
void press_w();
void press_e();
void press_r();
void press_t();
...


Parece una exageración tener 102 slots, todos prácticamente con el mismo código. Pero, ¿a caso se te ocurre alguna alternativa mejor? Para hacer esto de una forma mas eficiente y sencilla de mantener, con un único slot, se usa la clase QSignalMapper. Con su ayuda, podríamos hacer esto:

    signalMapper = new QSignalMapper;

signalMapper->setMapping(key_q, QChar('q'));
signalMapper->setMapping(key_w, QChar('w'));
signalMapper->setMapping(key_e, QChar('e'));
signalMapper->setMapping(key_r, QChar('r'));
signalMapper->setMapping(key_t, QChar('t'));
...

// Conexiones de los button con el signal mapper
connect(key_q, SIGNAL(pressed()), signalMapper, SLOT(map());
connect(key_w, SIGNAL(pressed()), signalMapper, SLOT(map());
connect(key_e, SIGNAL(pressed()), signalMapper, SLOT(map());
connect(key_r, SIGNAL(pressed()), signalMapper, SLOT(map());
connect(key_t, SIGNAL(pressed()), signalMapper, SLOT(map());

// Y conexion del signal mapper con el slot genérico
connect(signalMapper, SIGNAL(mapped(QChar)), panelTexto, SLOT(setText(QChar)));


Y en el slot setText() irias pintando los distintos caracteres en el objeto "panelTexto", según la tecla pulsada por el usuario.

Visitas:

Seguidores