Saltar al contenido

Curso de creación de GUIs con Qt. Capítulo 09: Signals y Slots

[Nuevamente, para este artículo he contado con la colaboración de Cristián Maureira-Fredes que ha revisado todo y me ha dado una serie de correcciones y mejoras que hacen que el capítulo esté bastante mejor que el original. Cristián trabaja como ingeniero de software en el proyecto Qt for Python dentro de The Qt Company]

Lo interesante de los GUIs es que podamos hablarle a la computadora de una forma amigable y para poder establecer esta comunicación es necesario que cuando nosotros hagamos algo la aplicación responda de alguna forma. En este capítulo voy a dar un repaso al sistema de gestión de eventos en Qt5 que se conoce como Signals/Slots y que podemos ver como los conjuntos de acciones y las posibles reacciones que desencadenarán estas acciones.

Índice:

[Los materiales para este capítulo los podéis descargar de aquí]

[INSTALACIÓN] Si todavía no has pasado por el inicio del curso, donde explico cómo poner a punto todo, ahora es un buen momento para hacerlo y después podrás seguir con esta nueva receta.

Vamos a empezar a definir un poco qué es todo esto de la gestión de eventos para centrarnos y luego ya toquetearemos algo de código. Sin pretender ser muy estrictos:

  • Sucede algo: pulsamos sobre algún botón del ratón, ponemos el puntero del ratón sobre algún elemento, presionamos determinada tecla, emitimos un sonido,…). Esto sería un evento que emite una señal (Signal) o una acción.
  • Al ocurrir alguna acción, como las descritas antes, se puede desencadenar una o más reacciones (o no, como ha venido pasando principalmente hasta ahora).

En el framework Qt, todo esto se conoce como el mecanismo de Signals y Slots y es la forma de comunicación entre objetos en Qt:

Se emite una señal, signal, cuando ocurre un evento específico. Los widgets de Qt vienen con muchas señales predefinidas. Por otro lado, un slot es una función o método (o clase, cualquier cosa que defina __call__) que se ejecutará como respuesta a una señal en particular. Al igual que con las señales, los widgets de Qt pueden tener slots predefinidos. Lo normal es que creemos subclases y reescribamos las señales y slots de los widgets para que se ajusten mejor a lo que necesitamos que hagan.

Vamos a usar un código muy simple para ver como funciona todo esto. Lo escribo y lo comento más abajo. El código está adaptado de este ejemplo en la página de Qt for python:

'''
Curso de creación de GUIs con Qt5 y Python

Author: Kiko Correoso
Website: pybonacci.org 
Licencia: MIT
'''

import os
os.environ['QT_API'] = 'pyside2'
import sys

from qtpy.QtWidgets import QApplication, QPushButton
from qtpy.QtCore import Slot

@Slot()
def say_hello():
 print("Button pulsado, ¡Hola!")

if __name__ == '__main__':

    app = QApplication(sys.argv)
    boton = QPushButton("Pulsa")
    boton.clicked.connect(say_hello)
    boton.show()
    sys.exit(app.exec_())

El anterior ejemplo no usa una ventana principal. Solo usa un Widget de botón que podemos pulsar (más sobre esto en próximas entregas, ahora un Widget solo es una pieza de Lego). Es un ejemplo mínimo para ver como funciona el mecanismo Signal-Slot. Como comento, la aplicación consiste de un único botón. La parte interesante aquí es la siguiente:


    boton.clicked.connect(say_hello)

Lo que estamos haciendo es conectar un objeto a un Slot que se llama say_hello que se ‘desencaderá’ cuando exista una señal (Signal), en este caso la señal de pulsar el botón, clicked.


El Slot, en este caso, es algo muy simple que muestra en pantalla un texto (usando un simple print):

@Slot()
def say_hello():
    print("Button pulsado, ¡Hola!")

Si guardamos el anterior código en un fichero que se llame main_00.py y lo ejecutamos de la siguiente forma desde la carpeta donde has guardado el fichero:

python main00.py

Veríamos algo que tendría esta pinta:

El ejemplo anterior es algo muy rápido para ver como funciona el mecanismo Signal-Slot. Un objeto emite una señal que llama a un Slot y este responde. Vamos a seguir viendo ejemplos sobre esto.

Signals y Slots predefinidos

Partimos, ahora, de un código que dejamos en el capítulo 7 y mejorado con lo que vimos en el capítulo 8 (quitamos los submenús que, de momento, no los vamos a usar):

'''
Curso de creación de GUIs con Qt5 y Python

Author: Kiko Correoso
Website: pybonacci.org 
Licencia: MIT
'''

import os
os.environ['QT_API'] = 'pyside2'
import sys
from pathlib import Path

from qtpy.QtWidgets import QApplication, QMainWindow, QAction
from qtpy.QtGui import QIcon
import qtawesome as qta


class MiVentana(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self._create_ui()

    def _create_ui(self):
        self.resize(500, 300)
        self.move(0, 0)
        self.setWindowTitle('Hola, QMainWindow')
        ruta_icono = Path('.', 'imgs', 'pybofractal.png')
        self.setWindowIcon(QIcon(str(ruta_icono)))
        self.statusBar().showMessage('Ready')
        self._create_menu()

    def _create_menu(self):        
        menubar = self.menuBar()
        # File menu and its QAction's
        file_menu = menubar.addMenu('&File')
        exit_action = QAction(qta.icon('fa5.times-circle'),
                              '&Exit',
                              self)
        exit_action.setShortcut('Ctrl+Q')
        exit_action.setStatusTip('Exit application')
        file_menu.addAction(exit_action)
        # Help menu and its QAction's
        help_menu = menubar.addMenu('&Help')
        about_action = QAction(qta.icon('fa5s.info-circle'),
                               '&Exit',
                               self)
        about_action.setShortcut('Ctrl+I')
        about_action.setStatusTip('About...')
        help_menu.addAction(about_action)

if __name__ == '__main__':

    app = QApplication(sys.argv)
    w = MiVentana()
    w.show()
    sys.exit(app.exec_())

Lo anterior tenía esta pinta:

En la anterior aplicación, muy simple y que no hace gran cosa, ya estamos usando una serie de Widgets, QMainWindow, QMenuBar, QStatusBar,… Como he comentado anteriormente, todavía no me he metido a explicar en detalle lo que es un Widget. Eso vendrá más adelante. De momento, lo podemos seguir viendo como pequeñas piezas de lego que nos ayudan a construir algo. Estos Widgets ya existentes y que estamos usando pueden venir con signals y/o slots ya predefinidos aunque todavía no hemos visto nada de esto explícitamente. Ahora vamos a usar alguno para que la aplicación empiece a tener algo de funcionalidad.

Si vamos a la documentación de, por ejemplo, QMenuBar vemos que la clase tiene muchas funciones o métodos. Ya hemos usado alguno de estos métodos como addMenu. Si seguimos un poco más abajo en la documentación vemos que hay una sección que se llama Signals. En esa sección se puede ver que existen dos métodos que se llaman hovered (estar encima) y triggered (desencadenado o detonado). Estos son eventos típicos que podríamos usar al pasar el puntero del ratón sobre el menú (hovered) o cuando pulsamos sobre el menú (triggered).

Por otro lado, si ahora vamos a la documentación de QApplication, vemos que dispone de algunos slots además de otras cosas. Los slots que tiene no nos interesan ahora pero si vamos a la sección de funciones estáticas vemos alguna que puede ser interesante como closeAllWindows.

Vamos a hacer que cuando pasemos el puntero sobre la barra de menús la aplicación se cierre. Sí, lo sé, algo muy inútil, pero es para ir viendo conceptos. En este caso, el evento será colocar el ratón sobre la barra de menús, eso disparará la señal hovered y queremos que cuando se dispare se cierre la aplicación.

Vamos a conectar una señal de un objeto, un QMenuBar en este caso, con una función de otro objeto, el método closeAllWindows de la instancia de QApplication. En este caso no es un slot pero nos vale como slot porque, como hemos comentado antes, cualquier cosa que funcione como un callable (función, método,…) será válido como slot. La conexión se hará de la siguiente forma:

objeto_que_emite_la_señal.señal_emitida.connect(funcion_a_usar_como_slot)

Un ejemplo más real de lo anterior podría ser que la función func se ejecute cuando pulsemos sobre un botón (que será una instancia de un Widget). Cogiendo el ejemplo inicial:

boton.clicked.connect(func)

Vamos a hacer lo que hemos comentado antes, que se cierre la aplicación cuando el puntero del ratón esté sobre la barra de menús, y así vemos un nuevo ejemplo con código real. Como siempre, pongo el código y destaco las líneas nuevas con el comentario ## NUEVA LÍNEA:

'''
Curso de creación de GUIs con Qt5 y Python

Author: Kiko Correoso
Website: pybonacci.org 
Licencia: MIT
'''

import os
os.environ['QT_API'] = 'pyside2'
import sys
from pathlib import Path

from qtpy.QtWidgets import QApplication, QMainWindow, QAction
from qtpy.QtGui import QIcon
import qtawesome as qta


class MiVentana(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self._create_ui()
        instance = QApplication.instance() ## NUEVA LÍNEA
        self.menuBar().hovered.connect(instance.closeAllWindows) ## NUEVA LÍNEA

    def _create_ui(self):
        self.resize(500, 300)
        self.move(0, 0)
        self.setWindowTitle('Hola, QMainWindow')
        ruta_icono = Path('.', 'imgs', 'pybofractal.png')
        self.setWindowIcon(QIcon(str(ruta_icono)))
        self.statusBar().showMessage('Ready')
        self._create_menu()

    def _create_menu(self):        
        menubar = self.menuBar()
        # File menu and its QAction's
        file_menu = menubar.addMenu('&File')
        exit_action = QAction(qta.icon('fa5.times-circle'),
                              '&Exit',
                              self)
        exit_action.setShortcut('Ctrl+Q')
        exit_action.setStatusTip('Exit application')
        file_menu.addAction(exit_action)
        # Help menu and its QAction's
        help_menu = menubar.addMenu('&Help')
        about_action = QAction(qta.icon('fa5s.info-circle'),
                               '&Exit',
                               self)
        about_action.setShortcut('Ctrl+I')
        about_action.setStatusTip('About...')
        help_menu.addAction(about_action)

if __name__ == '__main__':

    app = QApplication(sys.argv)
    w = MiVentana()
    w.show()
    sys.exit(app.exec_())

Si lo anterior lo guardáis en un fichero que se llame main01.py y, desde la carpeta donde tenéis el fichero, lo ejecutáis desde la línea de comandos haciendo:

python main01.py

Veréis nuestra ventana que he mostrado más arriba. Si ahora pasáis el puntero del ratón sobre la barra de menús veréis que se cierra la aplicación:

Esto sería un ejemplo, muy inútil, de cómo podemos hacer que el usuario interactúe con nuestra aplicación.

Explico las líneas nuevas:


        instance = QApplication.instance()
        self.menuBar().hovered.connect(instance.closeAllWindows)
  • QApplication.instance nos proporciona un puntero a la instancia de la aplicación.
  • self.menuBar().hovered.connect(instance.closeAllWindows) conecta el objeto con la función estática que hemos comentado antes (closeAllWindows) cuando ocurre la señal hovered (pasar por encima).

Creando un slot

Como he venido comentando, podemos usar nuestras propias funciones para que reaccionen a eventos. Ahora vamos a añadir un poco de funcionalidad para que nuestra aplicación empiece a ser un poco más útil. Pongo el código y destaco las líneas nuevas con el comentario de siempre, ## NUEVA LÍNEA:

'''
Curso de creación de GUIs con Qt5 y Python

Author: Kiko Correoso
Website: pybonacci.org 
Licencia: MIT
'''

import os
os.environ['QT_API'] = 'pyside2'
import sys
from pathlib import Path

from qtpy.QtWidgets import ( ## NUEVA LÍNEA
    QApplication, QMainWindow, QAction, QMessageBox ## NUEVA LÍNEA
) ## NUEVA LÍNEA
from qtpy.QtGui import QIcon
from qtpy.QtCore import Slot ## NUEVA LÍNEA
import qtawesome as qta


class MiVentana(QMainWindow):
    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self._create_ui()

    def _create_ui(self):
        self.resize(500, 300)
        #self.move(0, 0)
        self.setWindowTitle('Hola, QMainWindow')
        ruta_icono = Path('.', 'imgs', 'pybofractal.png')
        self.setWindowIcon(QIcon(str(ruta_icono)))
        self.statusBar().showMessage('Ready')
        self._create_menu()

    def _create_menu(self):
        menubar = self.menuBar()
        # File menu and its QAction's
        file_menu = menubar.addMenu('&File')
        exit_action = QAction(qta.icon('fa5.times-circle'),
                              '&Exit',
                              self)
        exit_action.setShortcut('Ctrl+Q')
        exit_action.setStatusTip('Exit application')
        exit_action.triggered.connect( ## NUEVA LÍNEA
            QApplication.instance().closeAllWindows ## NUEVA LÍNEA
        ) ## NUEVA LÍNEA
        file_menu.addAction(exit_action)
        # Help menu and its QAction's
        help_menu = menubar.addMenu('&Help')
        about_action = QAction(qta.icon('fa5s.info-circle'), ## NUEVA LÍNEA
                               '&About', ## NUEVA LÍNEA
                               self) ## NUEVA LÍNEA
        about_action.setShortcut('Ctrl+I')
        about_action.setStatusTip('About...')
        about_action.triggered.connect( ## NUEVA LÍNEA
            self._show_about_dialog ## NUEVA LÍNEA
        ) ## NUEVA LÍNEA
        help_menu.addAction(about_action)

    @Slot()
    def _show_about_dialog(self): ## NUEVA LÍNEA
        msg_box = QMessageBox() ## NUEVA LÍNEA
        msg_box.setIcon(QMessageBox.Information) ## NUEVA LÍNEA
        msg_box.setText("Pybonacci app v -37.3") ## NUEVA LÍNEA
        msg_box.setWindowTitle("Ejemplo de Slot") ## NUEVA LÍNEA
        msg_box.setStandardButtons(QMessageBox.Close) ## NUEVA LÍNEA
        msg_box.exec_() ## NUEVA LÍNEA

if __name__ == '__main__':

    app = QApplication(sys.argv)
    w = MiVentana()
    w.show()
    sys.exit(app.exec_())

Con respecto al anterior código han desaparecido algunas líneas puesto que no eran muy útiles y no forman parte de este nuevo ejemplo.

Voy a pasar a explicar un poco el código nuevo:


from qtpy.QtWidgets import (
    QApplication, QMainWindow, QAction, QMessageBox
)

Importo QMessageBox. QMessageBox es un nuevo Widget que no voy a explicar ahora ya que se explicará en detalle más adelante. Ya sabéis, una nueva pieza de Lego, de momento.


from qtpy.QtCore import Slot ## NUEVA LÍNEA

Importamos Slot como hemos hecho con el primer ejemplo del botón.


        #self.move(0, 0)

Esa línea, _create_ui la comento para que la ventana no aparezca arriba a la derecha. De momento la dejo solo comentada.


        exit_action.triggered.connect(
            QApplication.instance().closeAllWindows
        )

En el método _create_menu hemos añadido la anterior línea a la QAction de salida de la aplicación. Cuando pulsemos sobre Exit en el menú File la aplicación se cerrará. Es lo mismo que hemos hecho antes cuando pasábamos el ratón sobre la barra de menús pero ahora usamos el slot en un lugar que tenga más sentido.


        about_action = QAction(qta.icon('fa5s.info-circle'), ## NUEVA LÍNEA
                               '&About', ## NUEVA LÍNEA
                               self) ## NUEVA LÍNEA

También en el método _create_menu, simplemente cambio &Exit por &About. El anterior &Exit no pintaba nada ahí y ahora lo estoy corrigiendo para que tenga sentido.


        about_action.triggered.connect(
            self._show_about_dialog
        )

También en el método _create_menu hemos añadido la anterior línea a la QAction de información de la aplicación. Cuando pulsemos sobre About en el menú Help la aplicación llamará al método _show_about_dialog.


    @Slot()
    def _show_about_dialog(self):
        msg_box = QMessageBox()
        msg_box.setIcon(QMessageBox.Information)
        msg_box.setText("Pybonacci app v -37.3")
        msg_box.setWindowTitle("Ejemplo de Slot")
        msg_box.setStandardButtons(QMessageBox.Close)
        msg_box.exec_()

Y, por último, añadimos un nuevo método, _show_about_dialog que se ejecutará cuando pulsemos sobre la opción About del menú Help. De momento no entro en detalles sobre QMessageBox pero veremos una ventanita con algo de información y con un único botón.

Si lo anterior lo guardáis en un fichero que se llame main02.py y, desde la carpeta donde tenéis el fichero, lo ejecutáis desde la línea de comandos haciendo:

python main02.py

Veréis de nuevo la ventana. Si ahora pulsáis en Help > About os debería salir una ventanita con un botón. Si la cerráis y luego pulsáis sobre File > Exit se debería cerrar la aplicación:

Bueno. Parece que nuestra aplicación, poco a poco, va siendo más útil o, al menos, hace cosas algo más interesantes.

¿Es necesario usar el decorador Slot en los callables?

La aplicación anterior funcionaría perfectamente si no usamos el decorador Slot. Entonces, ¿para qué usarlo? Leyendo la documentación de PyQt5 nos dan varias razones:

  • Podemos definir tipos y otras cosas (no voy a entrar en esto ahora).
  • Puede ahorrar memoria.
  • Puede ser más rápido.

[INCISO]

En PyQt5, las señales y slots, se definen usando pyqtSignal y pyqtSlot y se importan así:

from PyQt5.QtCore import pyqtSignal, pyqtSlot

En Qt for Python o PySide2 se definen usando Signal y Slot y se importan así:

from PySide2.QtCore import Signal, Slot

Eventos

Ahora voy a hacer un pequeño inciso en la explicación del mecanismo Signals/Slots para hablar muy superficialmente de los eventos en Qt.

Los eventos son un mecanismo parecido a las señales y slots para conseguir cosas similares pero es de más bajo nivel y con fines un poco diferentes.

Las señales y slots son ideales cuando queremos responder a alguna acción que quiera hacer el usuario pero sin preocuparnos mucho en los detalles sobre cómo el usuario ha pedido las cosas. También podemos usar las señales y los slots para personalizar ligeramente algún comportamiento de algún Widget.

Por otro lado, si necesitamos que el comportamiento cambie drásticamente, normalmente cuando estamos creando nuestros propios Widgets, necesitaremos gestionar los eventos de una forma más cruda (más bajo nivel).

Por tanto, los eventos se encuentran a nivel de clase y todas las instancias de clase reaccionarán de la misma forma al evento en cuestión. Por otro lado, las señales se establecen en el objeto, instancia de clase, y cada objeto podrá tener su propia conexión entre señal y slot. Podéis echarle un ojo a esta respuesta en StackOverflow si queréis algo más formal. Este párrafo es una traducción del primer comentario a esa respuesta que me ha parecido una buena síntesis del tema. También le podéis echar un ojo a la documentación oficial.

No quiero profundizar más ahora sobre esto para no complicar más este capítulo. Supongo que lo veremos más adelante en algún capítulo futuro usando ejemplos concretos.

Resumen

Lo que me gustaría que quedase claro es lo siguiente:

  • El mecanismo de alto nivel para lidiar con eventos en Qt se conoce como el mecanismo Signal-Slot.
  • Los eventos ocurrirán, principalmente, cuando el usuario quiera interactuar con la aplicación.
  • Una señal se emite cuando sucede un evento en particular. Un evento, como hemos visto, puede ser un click, posar el puntero sobre algo,…
  • Un slot puede ser cualquier callable Python. Funciones, métodos,… Cualquier cosa que implemente el método __call__.
  • Un slot se ejecutará cuando se emite la señal a la que está conectado.

Por tanto, un evento dispara una señal, la señal puede avisar a uno o más slots si los hemos conectado con la señal. El slot o los slots son los que lidiaran con el evento.

Y, por hoy, creo que ya es suficiente.

2 comentarios en «Curso de creación de GUIs con Qt. Capítulo 09: Signals y Slots»

  1. Hola. Estoy tratando de lanzar un slog al pulsar la tecla enter cuando tengo el foco en un botón. El caso es que creo que debería crear un evento específico para ellos pues los QPushButton no tienen un evento para esto.

    ¿Es posible crear eventos propios? En caso afirmativo, ¿Como se harían? Gracias y enhorabuena por la web!

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

+ eight = nine

Pybonacci