Механизм сигналов и слотов представляет одну из отличительных особенностей Qt и позволяют сделать приложение отзывачивым, реагировать на действия пользователя, отслеживать различные события в приложении. Так, когда пользователь выполняет какое-либо действие с каким-либо элементом пользовательского интерфейса, должна быть выполнена определенная задача. Например, если пользователь нажимает кнопку "Закрыть" в верхнем правом углу окна, то ожидается, что окно закроется. То есть необходим механизм для отслеживания событий и реагирования на них. В среде Qt такой механизм предоставляют сигналы и слоты.
Сигнал — это сообщение, которое передается, чтобы сообщить об изменении состояния объекта. Сигнал может нести информацию о произошедшем изменении.
Слот — это специальная функция, вызываемая в ответ на определенный сигнал. Поскольку слоты — это функции, они содержат логику для выполнения определенного действия.
Встроенные виджеты Qt имеют множество предопределенных сигналов. Но также можно расширять имеющиеся классы и добавлять к ним свои собственные сигналы. Аналогичным образом можно добавить свои собственные слоты для обработки сигнала. Сигналы и слоты упрощают реализацию паттерна Observer (Наблюдатель), избегая при этом шаблонного кода.
Графически связь слотов и сигналов можно представить следующим образом:
Все классы, наследуемые от QObject или одного из его подклассов (например, QWidget), могут содержать сигналы и слоты. Для определения сигнала в классе применяется специальная секция signals:
class MyClass : public QObject { Q_OBJECT public: MyClass(){} signals: void signalName(); // определяем сигнал };
Синтаксически сигнал представляет определение функции без тела. И для нее не надо определять реализацию.
В качестве слота может выступать потенциально любая функция, которая соответствует сигнатуре сигнала. Но для явного определения слота в классе можно использовать секцию slots:
Обычно сигналы генерируются объектами, когда они меняют свое состояние. Причем объект-генератор сигнала не знает и не заботится о том, получает ли другой объект сгенерированный сигнал. Благодаря такой композиции можно создавать независимые компоненты.
class MyClass : public QObject { Q_OBJECT public: MyClass(){} signals: void signalName(); // определяем сигнал public slots: void slotName(){ некоторый код } // определяем слот };
При этом для одного сигнала можно подключить множество слотов. Аналогично один слот может обрабатывать несколько сигналов. Можно даже подключить сигнал напрямую к другому сигналу. (При этом второй сигнал будет генерироваться сразу же, как только будет излучен первый.)
Если к одному сигналу подключено несколько слотов, то функции-слоты будут выполняться в порядке подключения к сигналу.
Причем чтобы класс мог определять сигналы и слоты, класс не только должен наследоваться от QObject, но и внутри тела класса применять макрос Q_OBJECT
.
Специальный инструмент - Meta-Object Compiler (moc) генерирует дополнительный код для производных классов QObject. Инструмент считывает заголовочные файлы C++ и, если находит макрос Q_OBJECT, создает другой исходный файл C++ с
метаобъектным кодом, который применяется для управления классами и объектами.
Чтобы сгенерировать сигнал, применяется специальный макрос emit:
emit signalName();
Чтобы подключить сигнал к слоту, можно использовать функцию QObject::connect()
, которая имеет ряд версий. Возьмем наиболее распространенную:
QMetaObject::Connection QObject::connect( const QObject *senderObject, const char *signalName, const QObject *receiverObject, const char *slotName, Qt::ConnectionType type = Qt::AutoConnection)
Данная функция принимает следующие параметры:
senderObject
: объект отправителя сигнала
signalName
: название отправленного сигнала
receiverObject
: объект-получатель сигнала
slotName
: метод слота, который обрабатывает сигнал
type
: тип устанавливаемого соединения. Он определяет, будет ли уведомление доставлено в слот немедленно или поставлено в очередь на потом.
В Qt можно создать шесть различных типов соединений:
Qt::AutoConnection: тип соединения по умолчанию, определяется автоматически при генерации сигнала. Если и отправитель, и получатель
находятся в одном потоке, то используется Qt::DirectConnection
, иначе применяется Qt::QueuedConnection
.
Qt::DirectConnection: в этом случае и сигнал, и слот находятся в одном потоке. Слот вызывается сразу после генерации сигнала.
Qt::QueuedConnection: в этом случае слот находится в разных потоках. Слот вызывается, как только управление возвращается в цикл обработки событий потока получателя.
Qt::BlockingQueuedConnection: аналогичен Qt::QueuedConnection
за тем исключением, что поток сигнала блокируется до тех пор, пока слот
не будет выполнен. Это соединение нельзя использовать, если отправитель и получатель находятся в одном потоке, чтобы избежать взаимоблокировки.
Qt::UniqueConnection: его можно комбинировать с любым из вышеупомянутых типов соединения, используя побитовую операцию ИЛИ. Применяется, чтобы избежать дублирования соединений. Соединение завершится неудачно, если оно уже существует.
Qt::SingleShotConnection: одноразовая обработка сигнала. В этом случае слот вызывается только один раз, и соединение разрывается после генерации сигнала. Данный тип можно использовать с другими типами соединений. Этот тип соединения был добавлен в Qt 6.0.
Существует несколько способов соединения сигналов и слотов. Наиболее часто используемый синтаксис выглядит следующим образом:
QObject::connect(this, SIGNAL(signalName()), this, SLOT(slotName()));
В данном случае при указании сигнала и функции слота применяются соответственно макросы SIGNAL()
и SLOT()
. Это наиболее старый синтаксис, применяемый с первых версий Qt.
В последних версиях Qt рекоммендуется применять другой синтаксис:
connect(sender, &MyClass::signalName, this, &MyClass::slotName);
В качестве слота также можно указать лямбда-выражение:
connect(sender, &MyClass::signalName, this, [=]() { sender->doSomething(); });
Чтобы убедиться, что сигнал успешно подключен к слоту, можно проверить возвращаемое значение. Соединение может быть не установлено, если сигнатуры несовместимы или отсутствуют сигнал и слот.
Сигнатуры сигналов и слотов могут содержать аргументы, и эти аргументы могут иметь значения по умолчанию. Cигнал можно подключить к слоту, если сигнал имеет как минимум столько же аргументов, сколько и слот, а также если существует возможность неявного преобразования между типами соответствующих аргументов. Возможные соединения сигнала и слота:
connect(sender, SIGNAL(signalName(int)), this, SLOT(slotName(int))); connect(sender, SIGNAL(signalName(int)), this, SLOT(slotName())); connect(sender, SIGNAL(signalName()), this, SLOT(slotName()));
Но в следующем случае соединение не будет установлено, так как слот имеет больше параметров, чем сигнал:
connect(sender, SIGNAL(signalName()), this, SLOT(slotName(int)));
Для разрыва соединения между сигналом и слотом применяется функция disconnect(). Она также имеет несколько различных версий. В простейшей форме она принимает соединение, которое надо разорвать:
bool QObject::disconnect(const QMetaObject::Connection &connection)