[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:
- Instalación de lo que vamos a necesitar.
- Qt, versiones y diferencias.
- Hola, Mundo.
- Módulos en Qt.
- Añadimos icono a la ventana principal.
- Tipos de ventana en un GUI.
- Ventana inicial de carga o Splashscreen
- Menu principal. Introducción.
- Mejorando algunas cosas vistas.
- Gestión de eventos o Acción y reacción (este capítulo).
- Introducción a Designer.
- Los Widgets vistos a través de Designer: Primera parte.
- Los Widgets vistos a través de Designer: Segunda parte.
- Los widgets vistos a través de Designer: Tercera Parte.
- Los widgets vistos a través de Designer: Cuarta Parte.
- Los widgets vistos a través de Designer: Quinta Parte.
- Los widgets vistos a través de Designer: Sexta parte.
- TBD… (lo actualizaré cuando tenga más claro los siguientes pasos).
[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ñalhovered
(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.
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!
Sin saber lo que has intentado o más detalle sobre lo que quieres hacer es difícil darte una respuesta. ¿Esto te valdría? https://doc.qt.io/qtforpython/PySide6/QtGui/QKeyEvent.html