Перейти к содержанию

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

Теория

Общая

В обычных графических фреймворках реализована концепция функций обратного вызова (callback functions) - в результате действий пользователя вызываются обычные методы класса типа void. Чтобы сопоставить код с кнопкой, необходимо передать в функцию указатель на кнопку. Элементы графического интерфейса пользователя оказываются тесно связаны с функциональными частями программы. Для обеспечения связей сообщения и методов обработки используются макросы — карты сообщений. Примеры интерфейсов, где так сделано - Windows API, MFC.
В Qt препроцессор вставляет дополнительную информацию на место метки Q_OBJECT в описании класса.
Механизм сигналов и слотов в Qt основан на следующих принципах:

  • каждый класс, унаследованный от QObject, может иметь любое количество сигналов и слотов;
  • сообщения, посылаемые посредством сигналов, могут иметь множество аргументов любого зарегистрированного типа;
  • сигнал можно соединять с различным количеством слотов. Отправляемый сигнал поступит ко всем подсоединенным слотам;
  • слот может принимать сообщения от многих сигналов, принадлежащих разным объектам;
  • соединение сигналов и слотов можно производить в любой точке приложения;
  • сигналы и слоты являются механизмами, обеспечивающими связь между объектами. Связь также может выполняться между объектами, которые находятся в различных потоках;
  • при уничтожении объекта происходит автоматическое разъединение всех сигнально-слотовых связей. Это гарантирует, что сигналы не будут отправляться к несуществующим объектам.
    Особенности работы механизма сигналов и слотов следующие:
  • сигналы и слоты не являются частью языка C++, поэтому требуется запуск дополнительного препроцессора перед компиляцией программы (делается автоматически в Qt фреймворке);
  • отправка сигналов происходит медленнее, чем обычный вызов функции, который производится при использовании механизма функций обратного вызова;
  • в процессе компиляции не производится никаких проверок: имеется ли сигнал или слот в соответствующих классах или нет; совместимы ли сигнал и слот друг с другом и могут ли они быть соединены вместе. Об ошибке можно будет узнать лишь тогда, когда приложение будет запущено. Вся эта информация выводится на консоль.

Иллюстрации соединений

  • Один сигнал соединён с одинаковыми слотами разных объектов (наследников одного класса)
  • Один сигнал соединён с разными слотами разных объектов.

Сигналы

Сигналы (signals) - это методы, которые в состоянии осуществлять пересылку сообщений. Сигналы определяются в классе, как обычные методы, но без реализации. Они являются прототипами методов, содержащихся в заголовочном файле определения класса. Всю дальнейшую заботу о реализации кода для этих методов берет на себя препроцессор. Методы сигналов не должны возвращать каких-либо значений, поэтому перед именем метода всегда должно стоять void.

Сигнал не обязательно соединять со слотом. Если соединения не произошло, то он просто не будет обрабатываться. Подобное разделение отправляющих и получающих объектов исключает возможность того, что один из подсоединенных слотов каким-то образом сможет помешать объекту, отправившему сигналы. Библиотека предоставляет большое количество уже готовых сигналов для существующих элементов управления. В основном, для решения поставленных задач хватает этих сигналов, но иногда возникает необходимость реализации новых сигналов в своих классах.

Слоты

Слоты (slots) — это методы, которые присоединяются к сигналам. По сути, они являются обычными методами. Основное их отличие состоит в возможности принимать сигналы. Как и обычные методы, они определяются в классе как publicprivate или protected. Соответственно, перед каждой группой слотов должно стоять одно из ключевых слов private slots:protected slots: или public slots:

В слотах нельзя использовать параметры по умолчанию, например slotMethod (int n = 8), или определять слоты как static. Классы библиотеки содержат целый ряд уже реализованных слотов.

Подготовка

1. Создание проекта "Диалоговое окно"

  1. В Qt Creator на вкладке "Начало" жмём Create Project...
  2. Выбираем Приложение (Qt) -> Приложение Qt Widgets
  3. Печатаем название
  4. Система сборки - Cmake
  5. Подробнее. Это главный шаг, в нём мы выбираем базовый класс QDialog, галочки по умолчанию - правильные
  6. Перевод - нет, комплекты выбираем основной и оставляем галочки для Debug и Release, готово.

2. Создание класса для работы в сигнально-слотовой системе

  1. Должен быть h и cpp файл для класса. Иначе, метаобъектный компилятор неправильно сгенерирует обвязку для подключения.
  2. Наследуем класс от QObject. Только его наследники могут иметь сигналы и слоты.
  3. Вставляем макрос Q_OBJECT. С помощью него доступны секции public slots, private slots, signals
  4. Делаем стандартный конструктор ClassName(QObject *parent=nullptr) : QObject(parent){...} Таким образом, у нас строится иерархия всех объектов Qt и можно один вставлять в другой. Паттерн - компоновщик.

Чтобы было удобнее, через QtCreator можно выполнить создание класса, с нужными характеристиками. Нажимаем правой кнопкой на проект, затем выбираем Add New...

Выбираем Add Class C++, в появившемся окне вводим имя класса, указываем что мы - наследник от QObject и проверяем галочки:

Нажимаем далее, смотрим на созданный класс

//myclass.h
#ifndef MYCLASS_H
#define MYCLASS_H

//1. Подключили базовый класс, от которого наследуемся
#include <QObject>

//2. Пронаследовались
class MyClass : public QObject
{
    //3. Подключаем макрос для доступа к секциям сигналов-слотов
    Q_OBJECT
public:
    //4. Конструктор, который принимает объект родитель для компоновки всех объектов
    explicit MyClass(QObject *parent = nullptr);
    //5. пустая секция сигналов, компиляция должна пройти успешно
signals:
};

#endif // MYCLASS_H

//myclass.cpp
#include "myclass.h"

MyClass::MyClass(QObject *parent)
    : QObject{parent} //6. Вызываем конструктор родителя с переданным объектом-предком
{}

3. Поиск готовых сигналов-слотов

Для поиска по библиотечным классам, необходимо выбрать объект класса (или на форме выделить компонент) и нажать F1. Далее, переходим к списку сигналов-слотов. Например, мы ищем какой сигнал возникнет при нажатии на кнопку. Это объект класса PushButton. Заходим в справку, оказывается, там нет описаний сигналов и слотов. Переходим на предка, QAbstractButton. Сигналы - наследуются:

Отлично, предок определяет сигналы и слоты, один из них - нам поможет.

смотрим, сигнал называется click, параметров у него нет, класс для соединения надо будет указывать QAbstractButton.

4. Добавление собственных сигналов-слотов в класс

После того, как вы создали MyClass вы можете добавить самостоятельно сигналы и слоты исходя из будущей логики приложения. Сигналы вы определяете, когда происходит внутреннее событие, которое снаружи нельзя получить. Например, сработал внутренний таймер, файл появился в системе, произошло подключение к некоему адресу. Самый простой вариант - добавление сигналов-флагов.

//myclass.h
#ifndef MYCLASS_H
#define MYCLASS_H

#include <QObject>

class MyClass : public QObject
{
    Q_OBJECT
public:
    explicit MyClass(QObject *parent = nullptr);
    SomeData GetData() {...};
signals:
    void SomeChanged();
};

#endif // MYCLASS_H

Итак, мы добавили новый сигнал SomeChanged, который отрабатывает, когда что-то изменилось, а объект которому надо будет получать информацию вызывает уже методы по типу GetData. Либо, можно в сигнал передавать несложные параметры (int, QString).
Слоты мы добавляем для реакции на сигнал снаружи. Чаще всего это сигналы от пользователя - нажати кнопку, поставили флажок и т.п. Слоты делают с такой же сигнатурой как и сигнал. Например, для указанного выше click от кнопки будет слот-обработчик onClick. Конкретные реализации посмотрим уже на примерах.

Подключение

Соединение объектов осуществляется при помощи статического метода connect(), который определен в классе QObject. В общем виде, вызов метода connect() выглядит следующим образом:

QObject::connect(
const QObject* sender, //(1)
PointerToMemberFunction signal, //(2)
const QObject* receiver, //(3)
PointerToMemberFunction slot, //(4)
Qt::ConnectionType type = Qt::AutoConnection //(5)
);

Ему передаются пять следующих параметров:

  1. sender — указатель на объект, отправляющий сигнал;
  2. signal — это сигнал, с которым осуществляется соединение. Даётся указатель на метод класса, в котором определён сигнал. Должен находится в секции signals.
  3. receiver — указатель на объект, который имеет слот для обработки сигнала;
  4. slot — слот, который вызывается при получении сигнала. Даётся указатель на метод класса, в котором определён слот. Должен находится в секции public slots/protected slots/private slots.
  5. type — управляет режимом обработки. Имеется три возможных значения:
    • Qt::DirectConnection — сигнал обрабатывается сразу вызовом соответствующего метода слота
    • Qt::QueuedConnection — сигнал преобразуется в событие и ставится в общую очередь для обработки
    • Qt::AutoConnection — это автоматический режим, который действует следующим образом: если отсылающий сигнал объект находится в одном потоке с принимающим его объектом, то устанавливается режим Qt::DirectConnection, в противном случае — режим Qt::QueuedConnection. Этот режим (Qt::AutoConnection) определен в методе connection() по умолчанию.

Подключение сигналов от событий на форме к слоту на форме

Рассмотрим задачу: при изменении числового счётчика необходимо дополнительно менять лейбл со значением этого счётчика.
Окно выглядит примерно так:

Получается, у нас источник сигнала изменения значения spinBox, приёмник - label.
Смотрим, какие сигналы есть у spinBox. У него два сигнала - один с интовым значением, второй с текстовым. Вот сигнатура текстового textChanged(const QString &text).
Далее, смотрим какие слоты есть у лейбела. Там 7 разных слотов, есть подходящий - void setText(const QString &). Это значит, что мы можем напрямую прицепить сигнал от счётчика к лейбелу. Не нужно никуда дополнительно заводить. Так и поступим. Выбираем место где будем стыковать "разъёмы". Лучше всего делать в конструкторе окна, сразу после инициализации формы. Оттуда видны наши товарищи-виджеты. Дополняем код:

Dialog::Dialog(QWidget *parent)
    : QDialog(parent)
    , ui(new Ui::Dialog)
{
    ui->setupUi(this);
    //connect обязательно после `setupUi!`
    connect(
        ui->spinBox,
        &QSpinBox::textChanged,
        ui->label,
        &QLabel::setText
        );
}

)

Упражнения:

  • Сделать аналогичное подключение для doubleSpinBox и lineEdit
  • Сделать подключение чекбоксов. При нажатии на "главный" чек-бокс у трех соседних установится такое-же состояние как у него. Выбираем галочкой главный, проставились три галочки на соседних. Убираем галочку. Пропали галочки на соседних.

Подключение сигналов от событий на форме для обработке в диалоговом окне

Итак, задача следующая - необходимо при возникновении события на форме, например, нажатия кнопки выполнить более сложную обработку, ну или нет удобного слота в другом компоненте. Допустим, при нажатии на кнопку надо выдать в отладку "hello!" и поменять значение чек-бокса. Внешний вид следующий:

рассуждаем так. Нам необходимо получить сигнал от кнопки click и принять на форме. В справке нашли как выглядит сигнал QAbstractButton::click. Создаём слот в классе Dialog. Слот можно сделать приватным, снаружи никого не подключаем:

//dialog.h
#ifndef DIALOG_H
#define DIALOG_H

#include <QDialog>

QT_BEGIN_NAMESPACE
namespace Ui {
class Dialog;
}
QT_END_NAMESPACE

class Dialog : public QDialog
{
    Q_OBJECT
public:
    Dialog(QWidget *parent = nullptr);
    ~Dialog();

private slots: //секция слотов
    void on_click(); //сам обработчик
private:
    Ui::Dialog *ui;
};
#endif // DIALOG_H

В реализацию слота добавляем логику нажатия:

//dialog.cpp
#include <QDebug>
...
void Dialog::on_click()
{
    qDebug()<<"Clicked!"; //выводим лог
    ui->checkBox->setChecked(!ui->checkBox->isChecked()); //инвертируем состояние кнопки
}

Осталось присоединить нажатие кнопки к классу диалога. Так как диалог подключает сам себя, надо использовать this как указатель на объект-приёмник:

Dialog::Dialog(QWidget *parent)
    : QDialog(parent)
    , ui(new Ui::Dialog)
{
    ui->setupUi(this);

    connect(
        ui->pushButton,
        &QAbstractButton::clicked,
        this,
        &Dialog::on_click
        );
}

Результат:

Упражнения:

  • Сделайте модель "вентиля". У вас на форме будут два чек-бокса и лейбл. Если нажаты оба чек-бокса, то отображается надпись "открыт", если только один чек-бокс или нет, то надпись "закрыт"
  • Сделать аналоговый переключатель тепла. При изменении от минимума до максимума будет меняться пункт из комбобокс "холодно-тепло-горячо"
  • Усложнение - сделать обратную связь. Меняем комбобокс и устанавливается аналоговый переключатель в некое значение. То есть можем управлять теплом ручной или явным выбором.

Подключение сигналов и слотов с использованием встроенных классов

Новый вариант. У нас сигналы могут отправлять не только виджеты, но и другие сущности Qt. Чтобы это случилось необходимо:
1. Убедится что требуемый класс имеет сигналы. Например, в справочной системе
2. Создать объект через new
3. Выбрать целевой объект, проверить слоты
4. Создать связь через connect

Будем отслеживать изменение директории в системе, отобразим факт изменений в поле вывода. Для этого заведем на форме лейбл и editBox

Для отслеживания есть специальный класс QFileSystemWatcher. Добавляем в диалог:

#include <QFileSystemWatcher>

Dialog::Dialog(QWidget *parent)
    : QDialog(parent)
    , ui(new Ui::Dialog)
{
    ui->setupUi(this);
    QFileSystemWatcher* watcher = new QFileSystemWatcher(this);
    watcher->addPath("C:\\GIT");
    connect(watcher,
            &QFileSystemWatcher::directoryChanged,
            ui->lineEdit,
            &QLineEdit::setText);
}

Собираем и запускаем:

Упражнения:

  • Сделать программу, которая при изменнии файла отобразит его имя в label-виджете
  • Сделать программу, которая при срабатывании таймера (QTimer), через 2 секунды после запуска, установит галочку на форме

Подключение сигналов и слотов с использованием собственных классов

==TODO==

Добавить: граф сигналов-слотов
Пример: упрощённая модель автоматической коробки передач.
- Кнопка запуска
- Педаль тормоза
- Педаль газа
Вывод:
* состояние включено/выключено
* Обороты

Источники

add_circle2025-03-18update2025-03-20