Сигналы и слоты Qt5 - АлтунинВВ.Блог - всё об IT-технологиях!
Воскресенье, 06 сентября 2020 15:51

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

Россия

Создание интерфейсов пользователя (GUI) на любом языке программирования всегда ставит перед программистом проблему — необходимость реагировать на действия пользователя.

Например, это реакция программы на нажатие на кнопку. Когда пользователь ставит галочку в CheckBox – нужно включить, отключить или скрыть некоторые компоненты. При вводе текста, в поле ввода, проверить и отослать уведомление обработчику кнопки сохранения, чтобы тот проверил корректность ввода и сделал кнопку активной и так далее.

Сегодня мы рассмотрим механизм слотов и сигналов в Qt5.

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

Но у callback есть явные недостатки — механизм не стандартизирован и можно легко ошибиться указав неверный аргумент, что приведет к непредсказуемым и трудно обнаруживаемым ошибкам, так же вы можете столкнуться с проблемами при приведении типов. 

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

Лучше всего сигналы и слоты иллюстрирует картинка из документации Qt5.

 abstract-connections.png

И слоты и сигналы объявляются при создании класса виджета, при этом класс обязательно должен быть дочерним классом 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();
  • Третий параметр — объект класса с обработчиком события;
  • Четвертый параметр — слот — метод класса с кодом обработки события.
Прочитано 183 раз Последнее изменение Воскресенье, 06 сентября 2020 17:19