PyQt5: Signal e event
Torna all’indice degli appunti
Signals and Slots
Le GUI applications sono event-driven, ovvero vengono continuamente generati eventi (ad esempio il click dello user su un pulsante) che vengono catturati dal mainloop dell’applicazione (metodo exec_()) e spediti agli oggetti di competenza.
In PyQt5 la comunicazione tra oggetti avviene tramite il meccanismo Signal/Slot.
Signal: viene emesso quando avviene un determinato evento;
Slot: è una qualsiasi callable Python che, collegata opportunamente ad un segnale, viene chiamata
ogni volta che tale segnale viene emesso;
Le caratteristiche del meccanismo Signal/Slot, sono le seguenti:
1. Un Signal può essere connesso a più Slot;
2. Un Signal può essere connesso ad un altro Signal;
3. Gli argomenti del Signal possono essere tutti i tipi di Python;
4. Uno Slot può essere connesso a più Signals;
5. La connessione può essere diretta (sincrona) o queued (Asincrona);
6. La connessione può essere effettuata tra threads differenti;
7. Il Signal può essere disconnesso
Per definire un nuovo segnale si utilizza il pyqtSignal factory che prende in ingresso i seguenti argomenti:
types: può essere ogni Python type o una stringa che sia una C++ type signature, ad es. “QString”;
name: il nome del segnale, se omesso si utilizza il nome dell’attributo di classe;
revision: la revisione del segnale che è esportata in QML;
arguments: la sequenza dei nomi degli argomenti del Signal, che vengono esportati in QML;
>>> from PyQt5.QtCore import QObject, pyqtSignal >>> class MyQObject(QObject): ... mysignal = pyqtSignal() ... >>> MyQObject.mysignal <unbound PYQT_SIGNAL mysignal()> >>> myobject = MyQObject() >>> myobject.mysignal <bound PYQT_SIGNAL mysignal of MyQObject object at 0x034D6990>
Nel momento in cui viene istanziato MyQobject, mysignal diventa un PYQT_SIGNAL bound mettendo a disposizione i tre metodi fondamentali: connect, disconnect, emit.
connect: connette il segnale allo slot;
disconnect: disconnette il segnale;
emit: emette il segnale;
Supponiamo di avere questa semplice classe:
from PyQt5.QtCore import QObject, pyqtSignal class MyQObject(QObject): my_signal = pyqtSignal() def __init__(self): super(MyQObject, self).__init__() self.my_signal.connect(self.on_event) def my_event(self): print("Signal emitting...") self.my_signal.emit() def on_event(self): print("Signal received!")
Istanziamo l’oggetto e scateniamo l’evento:
>>> my_object = MyQObject() >>> my_object.my_event() Signal emitting... Signal received!
Se disconnettiamo il segnale…
>>> my_object.my_signal.disconnect() >>> my_object.my_event() Signal emitting...
Non viene più ricevuto.
Signal con argomenti
Abbiamo detto che pyqtSignal prende come argomento type
Vediamo un paio di esempi con int e con una stringa
from PyQt5.QtCore import QObject, pyqtSignal class MyQObject(QObject): my_signal = pyqtSignal(int) def __init__(self): super(MyQObject, self).__init__() self.my_signal.connect(self.on_event) def my_event(self): print("Signal emitting...") self.my_signal.emit(100) def on_event(self, value): print("Signal <%s> received!" % value) if __name__ == "__main__": my_object = MyQObject() my_object.my_event()
Eseguendo questo codice otteniamo:
Signal emitting... Signal <100> received!
Per utilizzare il type stringa, dobbiamo usare la C++ signature quindi:
from PyQt5.QtCore import QObject, pyqtSignal class MyQObject(QObject): my_signal = pyqtSignal('QString') def __init__(self): super(MyQObject, self).__init__() self.my_signal.connect(self.on_event) def my_event(self): print("Signal emitting...") self.my_signal.emit("bancaldo") def on_event(self, value): print("Signal <%s> received!" % value) if __name__ == "__main__": my_object = MyQObject() my_object.my_event()
Signal emitting... Signal <bancaldo> received!
Metter in comunicazione due Oggetti differenti
Supponiamo di volere far giungere il segnale emesso da un primo oggetto, ad un secondo oggetto.
Il primo oggetto è già definito e, una volta emesso il segnale, lo riceve correttamente.
Creiamo quindi un secondo oggetto e, in esso, un metodo che prenda in ingresso il riferimento al primo oggetto, così da effettuare una seconda connessione al segnale emesso dal primo oggetto, ma su uno slot del secondo oggetto.
Per aggiungere un ulteriore slot al segnale, si utilizza il decoratore pyqtSlot come nel codice seguente:
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot class MyQObject(QObject): my_signal = pyqtSignal('QString') def __init__(self): super(MyQObject, self).__init__() self.my_signal.connect(self.on_event) def my_event(self): print("[FIRST OBJECT] Signal emitting...") self.my_signal.emit("bancaldo") def on_event(self, value): print("[FIRST OBJECT] Signal <%s> received!" % value) class MySecondQObject(QObject): def __init__(self): super(MySecondQObject, self).__init__() def make_connection(self, external_object): external_object.my_signal.connect(self.get_external_value) @pyqtSlot('QString') def get_external_value(self, value): print("[SECOND OBJECT] Signal <%s> received!" % value) if __name__ == "__main__": my_object = MyQObject() second_object = MySecondQObject() second_object.make_connection(my_object) my_object.my_event()
che una volta eseguito:
[FIRST OBJECT] Signal emitting... [FIRST OBJECT] Signal <bancaldo> received! [SECOND OBJECT] Signal <bancaldo> received!
In questo esempio, nel secondo oggetto, abbiamo utilizzato il decoratore pyqtSlot, per definire la callable
get_external_value. Il codice funzionerebbe anche senza l’uso del decoratore; le differenze tra usare o meno il
decoratore sono:
– che il metodo apparirà nella lista degli slot in caso di utilizzo di una interfaccia QML;
– uso di memoria ridotto;
– facilità di lettura del codice;
Sender
Qualora volessimo risalire a chi ha emesso il segnale, è possibile utilizzare il metodo sender di QObject sender() ritorna semplicemente l’oggetto originale che ha mandato il segnale.
Nel nostro codice basta modificare come segue:
... class MyQObject(QObject): ... def __repr__(self): return "<%s>" % MyQObject.__name__ class MySecondQObject(QObject): ... @pyqtSlot('QString') def get_external_value(self, value): sender = self.sender() print("[SECOND OBJECT] Signal <%s> received from %s!" % (value, sender)) ...
che si traduce in:
[FIRST OBJECT] Signal emitting... [FIRST OBJECT] Signal <bancaldo> received! [SECOND OBJECT] Signal <bancaldo> received from <MyQObject>!
Reimplementazione EventHandler predefiniti
E’ possibile sovrascrivere il comportamento degli EventHandler predefiniti in modo da personalizzarli.
Ad esempio vogliamo che nella nostra textbox venga rilevata la pressione del tasto ESC e si esca dall’applicazione.
Il codice è il seguente:
from PyQt5.QtWidgets import QWidget, QApplication, QBoxLayout, QLineEdit from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMainWindow import sys class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.setWindowTitle("QLineEdit Example") self.central_widget = FormWidget(self) self.setCentralWidget(self.central_widget) self.resize(200, 100) class FormWidget(QWidget): def __init__(self, parent): super(FormWidget, self).__init__(parent) self.main_win = parent layout = QBoxLayout(QBoxLayout.TopToBottom) label = QLineEdit() layout.addWidget(label, 0) self.setLayout(layout) def keyPressEvent(self, event): key = event.key() if key == Qt.Key_Escape: print("INFO: ESC-key pushed (%s). Exiting!" % key) self.main_win.close() else: print("INFO: KEY pushed (%s)" % key) if __name__ == '__main__': app = QApplication(sys.argv) main_window = MainWindow() main_window.show() sys.exit(app.exec_())
Quando premiamo il tasto ESC l’applicazione si chiude regolarmente.
Event object
Essendo Event un oggetto python, a seconda del tipo di evento ne possiamo sfruttare gli attributi.
Nel caso di reimplementazione dell’evento di movimento del mouse mouseMoveEvent, potremmo ad esempio mostrare in una label le coordinate del mouse.
Il codice è il seguente:
from PyQt5.QtWidgets import QWidget, QApplication, QGridLayout, QLabel, QFrame from PyQt5.QtWidgets import QMainWindow from PyQt5.QtCore import Qt import sys class MainWindow(QMainWindow): def __init__(self, parent=None): super(MainWindow, self).__init__(parent) self.setWindowTitle("QLineEdit Example") self.central_widget = FormWidget(self) self.setCentralWidget(self.central_widget) self.resize(200, 100) class FormWidget(QWidget): def __init__(self, parent): super(FormWidget, self).__init__(parent) self.main_win = parent grid = QGridLayout() self.label = QLabel("x: {0}, y: {1}".format(0, 0)) grid.addWidget(self.label, 0, 0, Qt.AlignTop) self.setMouseTracking(True) self.setLayout(grid) self.label.setFrameShape(QFrame.Panel) self.label.setFrameShadow(QFrame.Sunken) self.label.setLineWidth(2) def mouseMoveEvent(self, event): x = event.x() y = event.y() text = "x: {0}, y: {1}".format(x, y) self.label.setText(text) if __name__ == '__main__': app = QApplication(sys.argv) main_window = MainWindow() main_window.show() sys.exit(app.exec_())
Nota:
è fondamentale abilitare il setMouseTracking altrimenti non verrà tenuta traccia del mouse.
Esempio finale di segnale tra widget differenti
Niente di più che un piccolo esempio di comunicazione tra widgets:
import sys from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QProgressBar from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QSlider, QDialog, QLabel, QGridLayout, QHBoxLayout from PyQt5.QtCore import Qt, pyqtSignal class Monitor(QDialog): def __init__(self): super(Monitor, self).__init__() left_label = QLabel('L channel', self) right_label = QLabel('R channel', self) self.l_bar = QProgressBar(self) self.l_bar.setMaximum(100) self.l_bar.setMinimum(0) self.r_bar = QProgressBar(self) self.r_bar.setMaximum(100) self.r_bar.setMinimum(0) layout = QGridLayout(self) # Adding the widgets layout.addWidget(left_label, 0, 0) layout.addWidget(right_label, 1, 0) layout.addWidget(self.l_bar, 0, 1) layout.addWidget(self.r_bar, 1, 1) self.setLayout(layout) self.setWindowTitle('Channel Monitor') self.show() def make_connection(self, channel_object): channel_object.changedValue.connect(self.get_channel_value) @pyqtSlot(int) def get_channel_value(self, value): sender = self.sender() if sender.name.lower() == "left": self.l_bar.setValue(value) else: self.r_bar.setValue(value) class Channel(QDialog): changedValue = pyqtSignal(int) def __init__(self, name): super(Channel, self).__init__() self.name = name.lower() self.setWindowTitle("%s channel" % self.name) label = QLabel('volume', self) self.slider = QSlider(self) self.slider.setMinimum(0) self.slider.setMaximum(100) self.slider.setOrientation(Qt.Horizontal) layout = QHBoxLayout(self) layout.addWidget(label) layout.addWidget(self.slider) self.setLayout(layout) self.slider.valueChanged.connect(self.on_changed_value) self.show() def on_changed_value(self, value): self.changedValue.emit(value) if __name__ == '__main__': app = QApplication(sys.argv) monitor = Monitor() monitor.setGeometry(400, 300, 200, 100) left_channel = Channel("left") left_channel.setGeometry(400, 450, 200, 50) right_channel = Channel("right") right_channel.setGeometry(400, 550, 200, 50) monitor.make_connection(left_channel) monitor.make_connection(right_channel) sys.exit(app.exec_())
Commenti recenti