Слоты и сигналы

Наряду с событиями Qt поддерживает механизм слотов и сигналов, которые позволяют общаться Qt виджетам (и не только) общаться между собой.

Грубо говоря, принцип механизма слотов и сигналов можно объяснить на аналогии с тем, как работает искусственное освещение в помещениях. Результатом нажатия на выключатель (сигнал) Вы получаете включение (или выключение) лампы (слот).

В качестве другого примера непосредственно связанного с разработкой графического интерфейса можно привести нажатие на кнопку (например, QPushButton): нажатие на кнопку (click) — сигнал, а слот — то, что происходит при нажатии на кнопку.

У любого объекта QObject (или производного) могут быть сигналы. Сигналы излучаются, когда такой объект претерпевает какие-то изменения, которые могут быть интересны другим объектам. Излучение сигналов — единственное, что объект делает, для того чтобы общаться. Излучая сигнал, объект не знает, принимает ли кто-то этот сигнал или нет.

Все встроенные виджеты Qt предоставляют набор подходящих сигналов, но можно создавать и пользовательские. В качестве слота подходит любая python функция (Callable).

Нажатие на кнопку. Сигнал clicked

В качестве самого простого примера создадим кнопку QPushButton и сделаем так, чтобы при нажатии на неё в консоли выводилось сообщение.

Если бы мы пошли путем обработки событий, то нам бы пришлось наследовать от класса QPushButton и перегрузить в нем обработчик событий. При этом обработать все это адекватно может быть не так уж и легко: пользователь можно нажать на мышку, увести курсор за пределы кнопки, и отпустить — считать ли это за нажатие на кнопку?

Использование сигналов значительно упрощает обработку такого сценария. У QPushButton (а вообще говоря, у его базового класса QPushButton) есть сигнал clicked, который соответствует полноценному нажатию. При этом даже необязательно нажатие на кнопку должно осуществится, как результат взаимодействия пользователя с мышкой: программа сама может нажать на кнопку (метод click) или пользователь может нажать на пробел на клавиатуре, когда кнопка активна, сигнал clicked излучится и в этих случаях тоже.

Чтобы при нажатии на кнопку в консоли появлялось сообщение, необходимо соединить этот сигнал с соответствующим слотом, которым в данном случае будет являться функция, вызывающая в себе print. Соединение производится методом сигнала connect, которому в качестве аргумента передаётся слот.

import sys

from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton


def on_button_click():
    print("Button was pressed")


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        button = QPushButton("Press me!", parent=self)
        button.clicked.connect(on_button_click)
        self.setCentralWidget(button)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()

Рассмотрим детальнее этот пример.

button = QPushButton("Press me!", parent=self)
button.clicked.connect(on_button_click)

Первая из этих инструкций создаёт кнопку, а вторая из них соединяет сигнал clicked с функцией on_button_click:

def on_button_click():
    print("Button was pressed")

В итоге, при нажатии на кнопку излучается сигнал clicked, что приводит к вызову функции on_button_click.

Несколько сигналов и слотов

Дополнительным преимуществом сигналов и слотов перед событиями является то, что не только один и тот же сигнал можно соединять с произвольным количеством слотов, но и один и тот же слот может быть соединен с произвольным количеством сигналов.

В качестве примера создадим окно с тремя кнопками и двумя текстовыми виджетами, подсчитывающими нажатия на кнопки следующим образом: нажатие на левую кнопку увеличивает только левый счетчик, нажатие на правую — только правый счетчик, а нажатие на центральную кнопку — сразу обоих.

../_images/many_signals.png

Так как нам нужно два текстовых виджета, которые умеют считать, то реализуем их в виде класса. Расширим класс QLabel, чтобы он хранил в себе счетчик, а также его увеличение методом increment.

class CountingLabel(QLabel):
    def __init__(self, text):
        super().__init__(f"{text}\n{0:3d}")

        self.text = text
        self.setAlignment(Qt.AlignCenter)
        self.counter = 0

    def increment(self):
        self.counter += 1
        self.setText(f"{self.text}\n{self.counter:3d}")

Теперь необходимо создать окно с тремя кнопками, двумя такими текстовыми виджетами и соединить сигналы кнопок со слотами increment счетчиков по следующей схеме.

left_button.clicked.connect(left_label.increment)
middle_button.clicked.connect(left_label.increment)
middle_button.clicked.connect(right_label.increment)
right_button.clicked.connect(right_label.increment)

Т.е. сигнал средней кнопки соединяется и со слотом левой кнопки и со слотом правой кнопки.

Следующий код воспроизводит и желаемый функционал и внешний вид на картинке выше.

import sys

from PySide6.QtGui import Qt
from PySide6.QtWidgets import *


class CountingLabel(QLabel):
    def __init__(self, text):
        super().__init__(f"{text}\n{0:3d}")

        self.text = text
        self.setAlignment(Qt.AlignCenter)
        self.counter = 0

    def increment(self):
        self.counter += 1
        self.setText(f"{self.text}\n{self.counter:3d}")


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # top row
        left_label = CountingLabel("Left")
        right_label = CountingLabel("Right")

        # bottom row
        left_button = QPushButton("Increment left")
        middle_button = QPushButton("Increment both")
        right_button = QPushButton("Increment right")

        # layout
        main_layout = QVBoxLayout()

        top_layout = QHBoxLayout()
        top_layout.addWidget(left_label)
        top_layout.addWidget(right_label)
        main_layout.addLayout(top_layout)

        bottom_layout = QHBoxLayout()
        bottom_layout.addWidget(left_button)
        bottom_layout.addWidget(middle_button)
        bottom_layout.addWidget(right_button)
        main_layout.addLayout(bottom_layout)

        # connections
        left_button.clicked.connect(left_label.increment)
        middle_button.clicked.connect(left_label.increment)
        middle_button.clicked.connect(right_label.increment)
        right_button.clicked.connect(right_label.increment)

        widget = QWidget()
        widget.setLayout(main_layout)
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()

Встроенные слоты

У многих Qt виджетов есть предусмотренные разработчиками слоты. Например, упомянутый уже click у QPushButton. Более естественный пример — слот clear у текстового виджета QLineEdit: QLineEdit позволяет вводить строку текста, а метод clear очищает её.

Следующий код создаёт текстовое поле и кнопку, нажатие на которое очищает текстовое поле.

../_images/clear_slot.png
import sys

from PySide6.QtWidgets import *


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()

        # widgets
        line_edit = QLineEdit()
        clear_button = QPushButton("Clear")
        clear_button.clicked.connect(line_edit.clear)

        # layout
        layout = QHBoxLayout()
        layout.addWidget(line_edit)
        layout.addWidget(clear_button)

        widget = QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)


app = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
app.exec()