Создание интерфейсов пользователя (GUI) на любом языке программирования всегда ставит перед программистом проблему — необходимость реагировать на действия пользователя.
Например, это реакция программы на нажатие на кнопку. Когда пользователь ставит галочку в CheckBox – нужно включить, отключить или скрыть некоторые компоненты. При вводе текста, в поле ввода, проверить и отослать уведомление обработчику кнопки сохранения, чтобы тот проверил корректность ввода и сделал кнопку активной и так далее.
Сегодня мы рассмотрим механизм слотов и сигналов в Qt5.
В других языках программирования и фреймворках используются функции обратного вызова (callback) — это обычная функция, адрес которой сохраняется глобально или передается в методы любым возможным способом и, при возникновении события, она вызывается посредством специального механизма, в каждом языке это реализовано по своему.
Но у callback есть явные недостатки — механизм не стандартизирован и можно легко ошибиться указав неверный аргумент, что приведет к непредсказуемым и трудно обнаруживаемым ошибкам, так же вы можете столкнуться с проблемами при приведении типов.
В Qt был создан альтернативный механизм — сигналы и слоты. По началу он кажется сложным, позже, когда вы с ним разберетесь вы поймете, что он на самом деле очень удобен, а самое главное предсказуем и стандартен, к тому же все QWidgets используют сигналы и слоты.
Лучше всего сигналы и слоты иллюстрирует картинка из документации Qt5.

И слоты и сигналы объявляются при создании класса виджета, при этом класс обязательно должен быть дочерним классом QObject.
Простой пример использования
Допустим, у нас есть класс — машина. Каждый раз когда мы изменяем цвет машины, нам нужно, чтобы изменения отображались на экране. В данном случае в консоль будет выведено сообщение. Цвета будут представлены числом int, нам не важно какому числу соответствует какой цвет.
#ifndef CAR_H
#define CAR_H
#include <QObject>
class Car : public QObject
{
Q_OBJECT
private:
int color;
public:
explicit Car(QObject *parent = nullptr);
int getColor() const;
void setColor(int value);
public slots:
void SetColor(int value);
signals:
void colorChanged(int value);
};
#endif // CAR_H
#include "car.h"
int Car::getColor() const
{
return color;
}
void Car::setColor(int value)
{
color = value;
}
Car::Car(QObject *parent) : QObject(parent)
{
}
Теперь создадим класс CarRenderer, который будет «отрисовывать» нашу машину.
#ifndef CARRENDERRER_H
#define CARRENDERRER_H
#include <QObject>
class CarRenderrer : public QObject
{
Q_OBJECT
public:
explicit CarRenderrer(QObject *parent = nullptr);
void drawCar(int color);
public slots:
void redrawCarColor(int color);
signals:
};
#endif // CARRENDERRER_H
#include "carrenderrer.h"
#include <QDebug>
CarRenderrer::CarRenderrer(QObject *parent) : QObject(parent)
{
}
void CarRenderrer::redrawCarColor(int color)
{
qDebug() << "New car color =" << color;
}
И вот пример использования:
#include "car.h"
#include "carrenderrer.h"
int main(int argc, char *argv[])
{
Car *aCar= new Car();
CarRenderrer *renderer = new CarRenderrer();
QObject::connect(aCar, SIGNAL(colorChanged(int)), renderer, SLOT(redrawCarColor(int)));
aCar->setColor(0);
aCar->setColor(10);
aCar->setColor(15);
aCar->setColor(5);
return 0;
}
Запускаем, результат будет таким:
New car color = 0
New car color = 10
New car color = 15
New car color = 5
Давайте разберем как это работает.
Сначала в классе Car мы объявляем сигнал colorChanged(int value)
Этот сигнал должен вызываться, при наступлении какого-либо события. В нашем случае он вызывается сразу после изменения цвета:
void Car::setColor(int value)
{
color = value;
emit colorChanged(value);
}
Так же в классе CarRenderrer мы создали слот
void CarRenderrer::redrawCarColor(int color)
{
qDebug() << "New car color =" << color;
}
Затем, в основном коде, мы создаем экземпляры двух классов:
Car *aCar= new Car();
CarRenderrer *renderer = new CarRenderrer();
И вызываем статический метод connect, который связывает сигнал и слот:
QObject::connect(aCar, SIGNAL(colorChanged(int)), renderer, SLOT(redrawCarColor(int)));
Обратите внимание на эту функцию, передавать параметры в нее нужно строго в определенном порядке:
QObject::connect(объект_класса_источника_сигнала, SIGNAL(имя_сигнала(типы_переменных)), объект_класса_приемник_сигнала, SLOT(метод_слота(типы_переменных)));
Где объект_класса_источника_сигнала и объект_класса_приемник_сигнала — объекты класса, обязательно созданные до вызова QObject::connect, в противном случае могут возникать трудно отлавливаемые ошибки:
Например, если просто написать
Car *aCar;
то в консоли мы увидим:
13:56:45: Starting C:\projects\build-Slots_and_signals-Desktop_Qt_MinGW_w64_64bit_MSYS2-Debug\debug\Slots_and_signals.exe ...
13:56:47: The program has unexpectedly finished.
13:56:47: The process was ended forcefully.
13:56:47: C:\projects\build-Slots_and_signals-Desktop_Qt_MinGW_w64_64bit_MSYS2-Debug\debug\Slots_and_signals.exe crashed.
Программа просто упала, при этом без сообщения об ошибке.
Продолжим разбор кода.
Теперь каждый раз, когда мы вызываем метод
aCar→setColor();
Он в свою очередь делает emit сигнала colorChanged(value), в результате вызывается метод слота redrawCarColor(int color) и в консоль выводится сообщение:
New car color =
Все очень просто.
Несколько параметров в слоте
Вы можете передать несколько параметров в метод слота, для этого просто добавьте их в описание сигнала, слота и при вызове connect.
Например, при смене цвета машины, нам нужно выводить название этого цвета.
Изменим описание слота:
signals:
void colorChanged(int value, QString name);
Внесем изменения в метод
void Car::setColor(int value, QString name)
{
color = value;
emit colorChanged(value, name);
}
В данном случае нам не нужно хранить название цвета, поэтому мы просто его передаем в сигнале.
Внесем изменения в метод слота, изменив его объявление:
public slots:
void redrawCarColor(int color, QString name);
И реализацию:
void CarRenderrer::redrawCarColor(int color, QString name)
{
qDebug() << "New car color =" << color;
qDebug() << "New color name =" << name;
}
Изменим вызов connect
QObject::connect(aCar, SIGNAL(colorChanged(int,QString)), renderer, SLOT(redrawCarColor(int,QString )));
И вызовы методов:
aCar->setColor(0,"white");
aCar->setColor(10,"black");
aCar->setColor(15,"red");
aCar->setColor(5,"yellow");
Запускаем:
New car color = 0
New color name = "white"
New car color = 10
New color name = "black"
New car color = 15
New color name = "red"
New car color = 5
New color name = "yellow"
Работает!
Несколько слотов на одном сигнале
Один сигнал может использоваться несколькими слотами.
Например, нам нужно вести лог, изменения цвета машины.
#ifndef LOGGER_H
#define LOGGER_H
#include <QObject>
class Logger : public QObject
{
Q_OBJECT
public:
explicit Logger(QObject *parent = nullptr);
public slots:
void logColor(int color, QString name);
};
#endif // LOGGER_H
#include "logger.h"
#include <QDebug>
Logger::Logger(QObject *parent) : QObject(parent)
{
}
void Logger::logColor(int color, QString name)
{
qDebug() << "LOG: New car color =" << color;
qDebug() << "LOG: New color name =" << name;
}
Теперь мы можем просто добавить:
QObject::connect(aCar, SIGNAL(colorChanged(int,QString)), log, SLOT(LogColor(int,QString)));
Запустим
New car color = 0
New color name = "white"
LOG: New car color = 0
LOG: New color name = "white"
New car color = 10
New color name = "black"
LOG: New car color = 10
LOG: New color name = "black"
New car color = 15
New color name = "red"
LOG: New car color = 15
LOG: New color name = "red"
New car color = 5
New color name = "yellow"
LOG: New car color = 5
LOG: New color name = "yellow"
Таким образом, каждый раз по сигналу может вызываться любое количество слотов из разных классов.
Несколько сигналов на одном слоте
Давайте немного модернизируем классы, добавим ID для машины.
В класс Car добавим
private:
int id;
Добавим методы
int getId() const;
void setId(int value);
Изменим сигнал:
signals:
void colorChanged(int value, QString name, int id);
Метод:
void Car::setColor(int value, QString name)
{
color = value;
emit colorChanged(value, name, id);
}
Изменим слот:
public slots:
void redrawCarColor(int color, QString name, int id);
и реализацию:
void CarRenderrer::redrawCarColor(int color, QString name, int id)
{
qDebug() << "Car ID =" << id;
qDebug() << "New car color =" << color;
qDebug() << "New color name =" << name;
}
Изменим connect
QObject::connect(aCar, SIGNAL(colorChanged(int,QString,int)), renderer, SLOT(redrawCarColor(int,Qstring,int)));
Добавим:
aCar->setId(100);
Запускаем:
Car ID = 100
New car color = 0
New color name = "white"
LOG: New car color = 0
LOG: New color name = "white"
Car ID = 100
New car color = 10
New color name = "black"
LOG: New car color = 10
LOG: New color name = "black"
Car ID = 100
New car color = 15
New color name = "red"
LOG: New car color = 15
LOG: New color name = "red"
Car ID = 100
New car color = 5
New color name = "yellow"
LOG: New car color = 5
LOG: New color name = "yellow"
Теперь удалим
aCar->setId(100);
aCar->setColor(0,"white");
aCar->setColor(10,"black");
aCar->setColor(15,"red");
aCar->setColor(5,"yellow");
И добавим:
Car *aCar2 = new Car();
Car *aCar3 = new Car();
Car *aCar4 = new Car();
QObject::connect(aCar, SIGNAL(colorChanged(int,QString,int)), renderer, SLOT(redrawCarColor(int,QString,int)));
QObject::connect(aCar2, SIGNAL(colorChanged(int,QString,int)), renderer, SLOT(redrawCarColor(int,QString,int)));
QObject::connect(aCar3, SIGNAL(colorChanged(int,QString,int)), renderer, SLOT(redrawCarColor(int,QString,int)));
QObject::connect(aCar4, SIGNAL(colorChanged(int,QString,int)), renderer, SLOT(redrawCarColor(int,QString,int)));
aCar->setId(1);
aCar->setColor(0,"white");
aCar2->setId(2);
aCar2->setColor(10,"black");
aCar3->setId(3);
aCar3->setColor(15,"red");
aCar4->setId(4);
aCar4->setColor(5,"yellow");
Обратите внимание, это важно! Всегда сначала создавайте объект класса и сразу вызывайте connect, в противном случае можно потратить много времени в поисках причины, почему не вызывается слот!
Запускаем:
Car ID = 1
New car color = 0
New color name = "white"
LOG: New car color = 0
LOG: New color name = "white"
Car ID = 1
New car color = 0
New color name = "white"
Car ID = 2
New car color = 10
New color name = "black"
Car ID = 3
New car color = 15
New color name = "red"
Car ID = 4
New car color = 5
New color name = "yellow"
Как видите, логируется только aCar, остальные только выводят информацию о смене цвета.
Работа с Qwidgets
Давайте рассмотрим реальный пример — обработка нажатия на кнопку
Создадим новый проект Qt Widgets Application
Откроем mainwindow.cpp
Добавим слот
public slots:
void ButtonClicked();
и его реализацию:
void MainWindow::ButtonClicked()
{
qDebug() << "Button clicked";
}
и добавим после
ui->setupUi(this);
следующий код:
QPushButton *btn = new QPushButton(this);
btn->setText("Click me!");
connect(btn,SIGNAL(clicked()),this,SLOT(ButtonClicked()));
Запускаем. При нажатии на кнопку «Click me!» в консоли появится надпись:
Button clicked
Заключение
Сегодня мы, на примерах, рассмотрели использование слотов и сигналов в Qt5 для реализации механизма реакции на происходящие события.
Рассмотрели:
- простой пример с изменением цвета машины;
- пример с использованием нескольких слотов с одним сигналом;
- пример использования нескольких сигналов, одним слотом;
- реальный пример использования — обработка нажатие кнопки.
При использовании connect вы должны запомнить следующее:
- Первый параметр — объект класса, источника события;
- Второй параметр - сигнал события, например clicked();
- Третий параметр — объект класса с обработчиком события;
- Четвертый параметр — слот — метод класса с кодом обработки события.