БлогNot. Qt: работаем с сигналами и слотами

Qt: работаем с сигналами и слотами

Этот "классический" слегка доработанный пример на сигналы и слоты в Qt показывает, как их соединять, как разрывать и возобновлять соединение. Сначала немного теории.

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

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

Механизм сигналов и слотов основан на следующих принципах:

  • каждый класс, унаследованный от QObject, может иметь любое количество сигналов и слотов;
  • сообщения, посылаемые посредством сигналов, могут иметь множество аргументов любого типа;
  • сигнал можно соединять с различным количеством слотов. Отправляемый сигнал поступит ко всем подсоединенным слотам;
  • слот может принимать сообщения от многих сигналов, принадлежащих разным объектам;
  • соединение сигналов и слотов можно производить в любой точке приложения;
  • сигналы и слоты являются механизмами, обеспечивающими связь между объектами. Связь также может выполняться между объектами, которые находятся в различных потоках;
  • при уничтожении объекта происходит автоматическое разъединение всех сигнально-слотовых связей. Это гарантирует, что сигналы не будут отправляться к несуществующим объектам.

Особенности работы механизма сигналов и слотов следующие:

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

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

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

class MySignal {
 Q_OBJECT
 //...
 signals:
  void doIt();
 //...
};

Препроцессор обеспечит примерно такую реализацию сигнала:

void MySignal::doIt() {
 QMetaObject::activate(this, &staticMetaObject, 0, 0);
}

Выслать сигнал можно при помощи ключевого слова emit. Ввиду того, что сигналы играют роль вызывающих методов, конструкция отправки сигнала emit doIt() приведет к обычному вызову метода doIt(). Сигналы могут отправляться из классов, которые их содержат. Например, в листинге выше сигнал doIt() может отсылаться только объектами класса MySignal, и никакими другими. Чтобы иметь возможность отослать сигнал программно из объекта этого класса, следует добавить метод sendSignal(), вызов которого заставит объект класса MySignal отправлять сигнал doIt():

class MySignal {
 Q_OBJECT
 public:
  void sendSignal() {
   emit doIt();
  }
 signals:
  void doIt();
};

Сигналы также имеют возможность высылать информацию, передаваемую в параметре.

class MySignal : public QObject {
 Q_OBJECT
 public:
  void sendSignal() {
   emit sendString("Information");
  }
 signals:
  void sendString(const QString&);
};

Обратите внимание, что в прототипе функции-сигнала не указываются имена параметров, а только типы.

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

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

class MySlot : public QObject {
 Q_OBJECT
 public:
 MySlot();
 public slots:
  void slot() {
   qDebug() << "I’m a slot";
  }
};

Внутри слота вызовом метода sender() можно узнать, от какого объекта был выслан сигнал. Он возвращает указатель на объект типа QObject. Например, в этом случае на консоль будет выведено имя объекта, выславшего сигнал:

void slot() {
 qDebug() << sender()->objectName();
}

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

QObject::connect(const QObject* sender,
const char* signal,
const QObject* receiver,
const char* slot,
Qt::ConnectionType type = Qt::AutoConnection
);

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

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

Как может быть осуществлено соединение объектов в программе:

void main() {
 QObject::connect(pSender, SIGNAL(signalMethod()),pReceiver, SLOT(slotMethod()));
}

Если вызов происходит из класса, унаследованного от QObject, тогда указание QObject:: можно опустить:

MyClass::MyClass() : QObject() {
 connect(pSender, SIGNAL(signalMethod()),pReceiver, SLOT(slotMethod()));
}

В случае, если слот содержится в классе, из которого производится соединение, то можно воспользоваться сокращенной формой метода connect(), опустив третий параметр (pReceiver), указывающий на объект-получатель. Другими словами, если в качестве объекта-получателя должен стоять указатель this, его можно просто не указывать:

MyClass::MyClass() : QObject() {
 connect(pSender, SIGNAL(signalMethod()), SLOT(slot()));
}

void MyClass::slot() {
 qDebug() << "I’m a slot";
}

Иногда возникают ситуации, когда объект не обрабатывает сигнал, а просто передает его дальше. Для этого необязательно определять слот, который в ответ на получение сигнала (при помощи emit) отсылает свой собственный. Можно просто соединить сигналы друг с другом. Отправляемый сигнал должен содержаться в определении класса:

MyClass::MyClass() : QObject() {
 connect(pSender, SIGNAL(signalMethod()), SIGNAL(mySignal()));
}

Отправку сигналов заблокировать можно на некоторое время, вызвав метод blockSignals() с параметром true. Объект будет "молчать", пока блокировка не будет снята тем же методом blockSignals() с параметром false. При помощи метода signalsBlocked() можно узнать текущее состояние блокировки сигналов.

Параметры в слот передаются из сигнала, если количество, порядок и типы этих параметров в сигнале и слоте совпадают (или в слоте их может быть меньше).

В качестве законченного примера приведём проект Counter. Он умеет увеличивать счётчик на QLabel по нажатию кнопки "Add", а также разрывать и восстанавливать обработку сигналов по нажатию кнопки "Connect"/"Disconnect". Когда соединение отсутствует, счётчик не увеличивается. После 10 увеличений счётчика приложение в любом случае завершается.

Файл counter.h
#ifndef COUNTER_H
#define COUNTER_H

#include <QObject>
#include <QLabel>
#include <QPushButton>

class Counter : public QObject
{
    Q_OBJECT
private:
 int Value;
 bool Connected;
public:
 Counter(QObject *parent=0);
 QLabel lbl;
 QPushButton cmd,cmd2;
public slots:
 void slotInc();
 void disconnector();
signals:
 void goodbye ();
 void counterChanged(int);
};

#endif // COUNTER_H
Файл counter.cpp
#include "counter.h"

Counter::Counter (QObject *parent) :  QObject(parent), Value(0) {
 QObject::connect(&cmd2, SIGNAL(clicked()),this, SLOT(disconnector()) );
 this->Connected = false;
 this->disconnector();
}

void Counter::slotInc() {
 if (this->Connected==true) {
  emit counterChanged(++this->Value);
  if (this->Value == 10) { emit goodbye(); } //ограничиваемся 10 нажатиями
 }
}

void Counter::disconnector() {
 if (this->Connected == true) {
  QObject::disconnect(&cmd, SIGNAL(clicked()),this, SLOT(slotInc()) );
  QObject::disconnect(this, SIGNAL(counterChanged(int)), &lbl, SLOT(setNum(int)));
                                       //метод setNum(int) есть в QLabel
  this->Connected = false;
  cmd2.setText("CONNECT");
 }
 else {
  QObject::connect(&cmd, SIGNAL(clicked()),this, SLOT(slotInc()) );
  QObject::connect(this, SIGNAL(counterChanged(int)), &lbl, SLOT(setNum(int)), Qt::DirectConnection );
  this->Connected = true;
  cmd2.setText("DISCONNECT");
 }
}
Файл main.cpp
#include <QApplication>
#include "counter.h"

int main(int argc, char *argv[]) {
 QApplication a(argc, argv);
 Counter counter;
 counter.lbl.setText("0");
 counter.lbl.move(100,100);
 counter.cmd.setText("ADD");
 counter.cmd.move(100,200);
 counter.cmd2.setText("DISCONNECT");
 counter.cmd2.move(100,300);

 counter.lbl.show();
 counter.cmd.show();
 counter.cmd2.show();
 QObject::connect(&counter, SIGNAL(goodbye()), &a, SLOT(quit()) );
 return a.exec();
}

Соединять сигналы со слотами, разумеется, не обязательно программно. В режиме дизайна формы нажмите клавишу F4 для доступа к интерфейсу управления сигналами и слотами. Там же можно добавить в список новые, заданные программистом слоты.

соединение сигналов и слотов в режиме дизайна формы
соединение сигналов и слотов в режиме дизайна формы

 Скачать этот пример в архиве .ZIP с проектом QT5 (2 Кб)

Проиллюстрируем также "визуальное" соединение сигналов со слотами на примере обработки текстового поля QLineEdit и кнопки QPushButton, размещённых на форме виджета:

вид формы в режиме дизайна
вид формы в режиме дизайна

Нажмём в режиме дизайна формы клавишу F4 или обведённую на рисунке кнопку "Изменение сигналов/слотов", затем зажмём левую кнопку мыши на поверхности PushButton и протянем красную линию в окно виджета:

вызов настройки соединения
вызов настройки соединения

После отпускания кнопки мыши появилось окно "Настройка соединения", слева выберем сигнал clicked(), а справа нажмём кнопку Изменить, затем в новом окне Сигналы/Слоты кнопку "+" под списком слотов. К виджету добавился слот slot1(), после нажатия OK он появился в окне настройки соединения:

окно настройки соединения
окно настройки соединения

После нажатия OK связь создана и отображена на форме, вернуться к обычном виду можно нажатием клавиши F3.

Если мы хотим просто автоматически создать пустую функцию-слот, достаточно нажать правой кнопкой мыши на PushButton и выбрать пункт меню "Перейти к слоту...", а затем сигнал clicked(), для которого создаётся слот.

В добавленной таким способом в модуль функции можно писать код, например:

void Widget::on_pushButton_clicked()
{
    QMessageBox msg; 
     //Не забудьте добавить #include <QMessageBox> в widget.h!
    msg.setText(ui->lineEdit->text());
    msg.exec();
}

Приложение готово к работе, по нажатию кнопки выполняется этот код:

приложение в работе
приложение в работе

Существенно также то, что в пятой версии QT стало можно применять запись соединения, основанную на указателях.

Запись с макросами:

connect(button, SIGNAL(clicked()), this, SLOT(slotButton()));

Запись на основе указателей:

connect(button, &QPushButton::clicked, this, &MainWindow::slotButton);

Преимущество второго варианта заключается в том, что имеется возможность определить несоответствие сигнатур и неверное наименование слота или сигнала ещё на стадии компиляции проекта, а не в процессе тестирования приложения.

 Более новый гайд по теме для Qt6 в формате PDF (342 Кб)

 Проект Qt6 MySignal.zip из этой статьи, развернуть архив .zip в новую папку проекта (2 Кб)

 Проект Qt6 Counter.zip из этой статьи, развернуть архив .zip в новую папку проекта (2 Кб)

28.04.2015, 17:25 [49142 просмотра]


теги: qt c++ программирование

К этой статье пока нет комментариев, Ваш будет первым