{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Слоты и сигналы\n", "\n", "Наряду с событиями `Qt` поддерживает механизм [слотов и сигналов](https://doc.qt.io/qtforpython/tutorials/basictutorial/signals_and_slots.html), которые позволяют общаться `Qt` виджетам (и не только) общаться между собой. \n", "\n", "Грубо говоря, принцип механизма слотов и сигналов можно объяснить на аналогии с тем, как работает искусственное освещение в помещениях. Результатом нажатия на выключатель (сигнал) Вы получаете включение (или выключение) лампы (слот).\n", "\n", "В качестве другого примера непосредственно связанного с разработкой графического интерфейса можно привести нажатие на кнопку (например, `QPushButton`): нажатие на кнопку (`click`) --- сигнал, а слот --- то, что происходит при нажатии на кнопку. \n", "\n", "У любого объекта `QObject` (или производного) могут быть сигналы. Сигналы излучаются, когда такой объект претерпевает какие-то изменения, которые могут быть интересны другим объектам. Излучение сигналов --- единственное, что объект делает, для того чтобы общаться. Излучая сигнал, объект не знает, принимает ли кто-то этот сигнал или нет.\n", "\n", "Все встроенные виджеты `Qt` предоставляют набор подходящих сигналов, но можно создавать и пользовательские. В качестве слота подходит любая `python` функция (`Callable`). \n", "\n", "## Нажатие на кнопку. Сигнал `clicked`\n", "\n", "В качестве самого простого примера создадим кнопку [QPushButton](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QPushButton.html) и сделаем так, чтобы при нажатии на неё в консоли выводилось сообщение. \n", "\n", "Если бы мы пошли путем обработки событий, то нам бы пришлось наследовать от класса `QPushButton` и перегрузить в нем обработчик событий. При этом обработать все это адекватно может быть не так уж и легко: пользователь можно нажать на мышку, увести курсор за пределы кнопки, и отпустить --- считать ли это за нажатие на кнопку? \n", "\n", "Использование сигналов значительно упрощает обработку такого сценария. У `QPushButton` (а вообще говоря, у его базового класса [QPushButton](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QAbstractButton.html)) есть сигнал [clicked](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QAbstractButton.html#PySide6.QtWidgets.PySide6.QtWidgets.QAbstractButton.clicked), который соответствует полноценному нажатию. При этом даже необязательно нажатие на кнопку должно осуществится, как результат взаимодействия пользователя с мышкой: программа сама может нажать на кнопку (метод [click](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QAbstractButton.html#PySide6.QtWidgets.PySide6.QtWidgets.QAbstractButton.click)) или пользователь может нажать на пробел на клавиатуре, когда кнопка активна, сигнал `clicked` излучится и в этих случаях тоже.\n", "\n", "Чтобы при нажатии на кнопку в консоли появлялось сообщение, необходимо соединить этот сигнал с соответствующим слотом, которым в данном случае будет являться функция, вызывающая в себе `print`. Соединение производится методом сигнала `connect`, которому в качестве аргумента передаётся слот.\n", "\n", "```python\n", "import sys\n", "\n", "from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton\n", "\n", "\n", "def on_button_click():\n", " print(\"Button was pressed\")\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "\n", " button = QPushButton(\"Press me!\", parent=self)\n", " button.clicked.connect(on_button_click)\n", " self.setCentralWidget(button)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "\n", "Рассмотрим детальнее этот пример. \n", "```python\n", "button = QPushButton(\"Press me!\", parent=self)\n", "button.clicked.connect(on_button_click)\n", "```\n", "Первая из этих инструкций создаёт кнопку, а вторая из них соединяет сигнал `clicked` с функцией `on_button_click`:\n", "```python\n", "def on_button_click():\n", " print(\"Button was pressed\")\n", "```\n", "В итоге, при нажатии на кнопку излучается сигнал `clicked`, что приводит к вызову функции `on_button_click`.\n", "\n", "## Несколько сигналов и слотов\n", "\n", "Дополнительным преимуществом сигналов и слотов перед событиями является то, что не только один и тот же сигнал можно соединять с произвольным количеством слотов, но и один и тот же слот может быть соединен с произвольным количеством сигналов.\n", "\n", "В качестве примера создадим окно с тремя кнопками и двумя текстовыми виджетами, подсчитывающими нажатия на кнопки следующим образом: нажатие на левую кнопку увеличивает только левый счетчик, нажатие на правую --- только правый счетчик, а нажатие на центральную кнопку --- сразу обоих. \n", "\n", "```{figure} ../_static/lecture_specific/qt/many_signals.png\n", "```\n", "\n", "Так как нам нужно два текстовых виджета, которые умеют считать, то реализуем их в виде класса. Расширим класс `QLabel`, чтобы он хранил в себе счетчик, а также его увеличение методом `increment`.\n", "```python\n", "class CountingLabel(QLabel):\n", " def __init__(self, text):\n", " super().__init__(f\"{text}\\n{0:3d}\")\n", "\n", " self.text = text\n", " self.setAlignment(Qt.AlignCenter)\n", " self.counter = 0\n", "\n", " def increment(self):\n", " self.counter += 1\n", " self.setText(f\"{self.text}\\n{self.counter:3d}\")\n", "```\n", "Теперь необходимо создать окно с тремя кнопками, двумя такими текстовыми виджетами и соединить сигналы кнопок со слотами `increment` счетчиков по следующей схеме. \n", "```python\n", "left_button.clicked.connect(left_label.increment)\n", "middle_button.clicked.connect(left_label.increment)\n", "middle_button.clicked.connect(right_label.increment)\n", "right_button.clicked.connect(right_label.increment)\n", "```\n", "Т.е. сигнал средней кнопки соединяется и со слотом левой кнопки и со слотом правой кнопки. \n", "\n", "Следующий код воспроизводит и желаемый функционал и внешний вид на картинке выше.\n", "```python\n", "import sys\n", "\n", "from PySide6.QtGui import Qt\n", "from PySide6.QtWidgets import *\n", "\n", "\n", "class CountingLabel(QLabel):\n", " def __init__(self, text):\n", " super().__init__(f\"{text}\\n{0:3d}\")\n", "\n", " self.text = text\n", " self.setAlignment(Qt.AlignCenter)\n", " self.counter = 0\n", "\n", " def increment(self):\n", " self.counter += 1\n", " self.setText(f\"{self.text}\\n{self.counter:3d}\")\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "\n", " # top row\n", " left_label = CountingLabel(\"Left\")\n", " right_label = CountingLabel(\"Right\")\n", "\n", " # bottom row\n", " left_button = QPushButton(\"Increment left\")\n", " middle_button = QPushButton(\"Increment both\")\n", " right_button = QPushButton(\"Increment right\")\n", "\n", " # layout\n", " main_layout = QVBoxLayout()\n", "\n", " top_layout = QHBoxLayout()\n", " top_layout.addWidget(left_label)\n", " top_layout.addWidget(right_label)\n", " main_layout.addLayout(top_layout)\n", "\n", " bottom_layout = QHBoxLayout()\n", " bottom_layout.addWidget(left_button)\n", " bottom_layout.addWidget(middle_button)\n", " bottom_layout.addWidget(right_button)\n", " main_layout.addLayout(bottom_layout)\n", "\n", " # connections\n", " left_button.clicked.connect(left_label.increment)\n", " middle_button.clicked.connect(left_label.increment)\n", " middle_button.clicked.connect(right_label.increment)\n", " right_button.clicked.connect(right_label.increment)\n", "\n", " widget = QWidget()\n", " widget.setLayout(main_layout)\n", " self.setCentralWidget(widget)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "\n", "## Встроенные слоты\n", "\n", "У многих `Qt` виджетов есть предусмотренные разработчиками слоты. Например, упомянутый уже `click` у `QPushButton`. Более естественный пример --- слот [clear](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html#PySide6.QtWidgets.PySide6.QtWidgets.QLineEdit.clear) у текстового виджета [QLineEdit](https://doc.qt.io/qtforpython/PySide6/QtWidgets/QLineEdit.html): `QLineEdit` позволяет вводить строку текста, а метод `clear` очищает её.\n", "\n", "Следующий код создаёт текстовое поле и кнопку, нажатие на которое очищает текстовое поле.\n", "```{figure} ../_static/lecture_specific/qt/clear_slot.png\n", "```\n", "\n", "```python\n", "import sys\n", "\n", "from PySide6.QtWidgets import *\n", "\n", "\n", "class MainWindow(QMainWindow):\n", " def __init__(self):\n", " super().__init__()\n", "\n", " # widgets\n", " line_edit = QLineEdit()\n", " clear_button = QPushButton(\"Clear\")\n", " clear_button.clicked.connect(line_edit.clear)\n", "\n", " # layout\n", " layout = QHBoxLayout()\n", " layout.addWidget(line_edit)\n", " layout.addWidget(clear_button)\n", "\n", " widget = QWidget()\n", " widget.setLayout(layout)\n", " self.setCentralWidget(widget)\n", "\n", "\n", "app = QApplication(sys.argv)\n", "main_window = MainWindow()\n", "main_window.show()\n", "app.exec()\n", "```\n", "\n" ] } ], "metadata": { "language_info": { "name": "python" }, "orig_nbformat": 4 }, "nbformat": 4, "nbformat_minor": 2 }