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 );
Ему передаются пять следующих параметров:
sender
— указатель на объект, отправляющий сигнал;signal
— это сигнал, с которым осуществляется соединение. Прототип (имя и аргументы) метода сигнала должен быть заключен в специальный макросSIGNAL(method())
;receiver
— указатель на объект, который имеет слот для обработки сигнала;slot
— слот, который вызывается при получении сигнала. Прототип слота должен быть заключен в специальном макросеSLOT(method())
;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 просмотра]