Home > PyQt5, python > PyQt5: Signal e event

PyQt5: Signal e event

14 Marzo 2019

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_())

Torna all’indice degli appunti

Categorie:PyQt5, python Tag: ,
I commenti sono chiusi.