Сигналы и слоты
Сигналы и слоты используются для обмена сообщениями между объектами. Механизм сигналов и слотов является центральным элементом Qt и, вероятно, отличается больше всего от функций, предоставляемых другими фреймворками. Сигналы и слоты обеспечиваются метаобъектной системой Qt.
Введение
В программировании графического интерфейса, когда мы изменяем один виджет, мы часто хотим, чтобы другой виджет был уведомлен. Более общо, мы хотим, чтобы объекты любого типа могли общаться друг с другом. Например, если пользователь нажимает кнопку Закрыть, мы, вероятно, хотим, чтобы функция close() окна была вызвана.
Другие инструментарии достигают такого рода обмена сообщениями с помощью обратных вызовов. Обратный вызов — это указатель на функцию, поэтому, если вы хотите, чтобы функция обработки уведомила вас о каком-либо событии, вы передаете указатель на другую функцию (обратный вызов) функции обработки. Затем функция обработки вызывает обратный вызов при необходимости. Хотя успешные фреймворки, использующие этот метод, существуют, обратные вызовы могут быть неинтуитивными и могут иметь проблемы с гарантией корректности типов аргументов обратного вызова.
Сигналы и слоты
В Qt у нас есть альтернатива технике обратных вызовов: мы используем сигналы и слоты. Сигнал генерируется, когда происходит конкретное событие. Виджеты Qt имеют множество предопределенных сигналов, но мы всегда можем наследовать от виджетов, чтобы добавить собственные сигналы. Слоты — это функции, которые вызываются в ответ на конкретный сигнал. Виджеты Qt имеют множество предопределенных слотов, но обычно практикуется наследовать от виджетов и добавлять собственные слоты, чтобы обрабатывать интересующие вас сигналы.
Механизм сигналов и слотов является безопасным по типу: сигнатура сигнала должна соответствовать сигнатуре принимающего слота. (На самом деле, слот может иметь более короткую сигнатуру, чем сигнал, который он получает, так как он может игнорировать дополнительные аргументы). Поскольку сигнатуры совместимы, компилятор может помочь нам обнаруживать несоответствия типов при использовании синтаксиса на основе указателей на функции. Синтаксис SIGNAL и SLOT на основе строк обнаружит несоответствия типов во время выполнения. Сигналы и слоты слабо связаны: класс, который испускает сигнал, ни знает, ни заботится о том, какие слоты его получают. Механизм сигналов и слотов Qt гарантирует, что если вы соедините сигнал со слотом, слот будет вызван с параметрами сигнала в нужное время. Сигналы и слоты могут принимать любое количество аргументов любого типа. Они полностью безопасны по типу.
Все классы, которые наследуют от QObject или одного из его подклассов (например, QWidget), могут содержать сигналы и слоты. Сигналы генерируются объектами, когда они изменяют свое состояние таким образом, который может представлять интерес для других объектов. Это всё, что делает объект для обмена сообщениями. Он не знает и не заботится о том, получает ли кто-либо сигналы, которые он испускает. Это истинная инкапсуляция информации и гарантирует, что объект может быть использован как программный компонент.
Слоты могут использоваться для получения сигналов, но они также являются обычными членами функций. Так же, как объект не знает, получает ли кто-либо его сигналы, слот не знает, есть ли у него какие-либо подключенные сигналы. Это гарантирует, что можно создавать действительно независимые компоненты с помощью Qt.
Вы можете подключить сколько угодно сигналов к одному слоту, и один сигнал может быть подключен к любому нужному количеству слотов. Можно даже подключить сигнал напрямую к другому сигналу. (Это вызовет второй сигнал сразу же, когда будет выпущен первый).
Вместе сигналы и слоты образуют мощный механизм программирования компонентов.
Сигналы
Сигналы генерируются объектом, когда его внутреннее состояние изменилось каким-либо образом, который может представлять интерес для клиента или владельца объекта. Сигналы являются общедоступными функциями и могут генерироваться из любого места, но мы рекомендуем генерировать их только из класса, определяющего сигнал, и его подклассов.
При генерации сигнала слоты, подключенные к нему, обычно выполняются немедленно, как обычный вызов функции. В этом случае механизм сигналов и слотов полностью независим от любого цикла событий графического интерфейса. Выполнение кода, следующего за emit оператором, произойдет после того, как все слоты вернутся. Ситуация немного отличается при использовании очередных подключений; в таком случае код, следующий за ключевым словом emit, будет продолжаться немедленно, а слоты будут выполнены позже.
Если несколько слотов подключены к одному сигналу, слоты будут выполняться один за другим в порядке их подключения, когда сигнал будет генерироваться.
Сигналы автоматически генерируются модулем moc и не должны реализовываться в файле .cpp. Они никогда не могут иметь возвращаемые типы (т.е. использовать void).
Примечание об аргументах: наш опыт показывает, что сигналы и слоты более повторно используемы, если они не используют специальные типы. Если QScrollBar::valueChanged() будет использовать специальный тип, например, гипотетический QScrollBar::Range, то он сможет быть подключен только к слотам, специально разработанным для QScrollBar. Подключение различных виджетов ввода друг к другу было бы невозможно.
Слоты
Слоты вызываются, когда генерируется сигнал, подключенный к нему. Слоты — это обычные функции C++ и могут быть вызваны обычным образом; их единственная особенность заключается в том, что сигналы могут быть подключены к ним.
Поскольку слоты являются обычными членами функций, они следуют обычным правилам C++, когда вызываются непосредственно. Однако, как слоты, они могут вызываться любым компонентом, независимо от уровня доступа, через подключение сигнал-слот. Это означает, что сигнал, генерируемый экземпляром произвольного класса, может вызвать частный слот в экземпляре несвязанного класса.
Вы также можете определить слоты как виртуальные, что, по нашему опыту, очень полезно на практике.
По сравнению с обратными вызовами, сигналы и слоты немного медленнее из-за большей гибкости, хотя разница для реальных приложений незначительна. В целом, генерирование сигнала, подключенного к некоторым слотам, примерно в десять раз медленнее, чем вызов получателей непосредственно с вызовами невиртуальных функций. Это накладные расходы, необходимые для поиска объекта подключения, для безопасной итерации по всем подключениям (т. е. проверки того, что последующие получатели не были уничтожены во время генерации), и для передачи любых параметров в общем формате. Хотя десять вызовов невиртуальных функций могут показаться много, это намного меньше накладных расходов по сравнению с любой операцией new или delete, например. Как только вы выполняете операцию со строкой, вектором или списком, которая в скрытом виде требует new или delete, накладные расходы сигналов и слотов составляют лишь очень небольшую часть общих затрат на вызов функции. То же самое верно, когда вы делаете системный вызов в слоте или косвенно вызываете более десяти функций. Простота и гибкость механизма сигналов и слотов стоят этих накладных расходов, которые ваши пользователи даже не заметят.
Обратите внимание, что другие библиотеки, которые определяют переменные под названием signals или slots могут вызвать предупреждения и ошибки компилятора при компиляции приложения на основе Qt. Для решения этой проблемы #undef проблемный препроцессорный символ.
Маленький пример
Минимальная декларация класса C++ может выглядеть так:
class Counter
{
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
void setValue(int value);
private:
int m_value;
}; Небольшой класс на основе QObject может выглядеть так:
#include <QObject>
class Counter : public QObject
{
Q_OBJECT
public:
Counter() { m_value = 0; }
int value() const { return m_value; }
public slots:
void setValue(int value);
signals:
void valueChanged(int newValue);
private:
int m_value;
}; Версия на основе QObject имеет такое же внутреннее состояние и предоставляет общедоступные методы для доступа к состоянию, но дополнительно поддерживает программирование компонентов с использованием сигналов и слотов. Этот класс может сообщить внешнему миру, что его состояние изменилось, выпустив сигнал, valueChanged(), и он имеет слот, к которому другие объекты могут посылать сигналы.
Все классы, содержащие сигналы или слоты, должны упоминать Q_OBJECT вверху своей декларации. Они также должны наследовать (прямо или косвенно) от QObject.
Слоты реализуются программистом приложения. Вот возможное реализация слота Counter::setValue():
void Counter::setValue(int value)
{
if (value != m_value) {
m_value = value;
emit valueChanged(value);
}
} Строка emit генерирует сигнал valueChanged() от объекта со значением как аргументом.
В следующем фрагменте кода мы создаём два объекта Counter и подключаем первый сигнал объекта valueChanged() ко второму слоту объекта setValue() с помощью QObject::connect():
Counter a, b;
QObject::connect(&a, &Counter::valueChanged,
&b, &Counter::setValue);
a.setValue(12); // a.value() == 12, b.value() == 12
b.setValue(48); // a.value() == 12, b.value() == 48 Вызов a.setValue(12) заставляет a генерировать сигнал valueChanged(12), который b получит в своём слоте setValue(), т.е. вызов b.setValue(12). Затем b генерирует тот же сигнал valueChanged(), но так как ни один слот не подключён к сигналу b's valueChanged(), сигнал игнорируется.
Обратите внимание, что функция setValue() устанавливает значение и генерирует сигнал только если value != m_value. Это предотвращает бесконечные циклы в случае циклических подключений (например, если b.valueChanged() был подключён к a.setValue()).
По умолчанию для каждого подключения генерируется сигнал; для дублирующихся подключений генерируются два сигнала. Вы можете разорвать все эти подключения одним вызовом disconnect(). Если вы передаёте тип Qt::UniqueConnection, подключение будет установлено только если оно не является дубликатом. Если уже существует дубликат (абсолютно тот же сигнал к абсолютно тому же слоту на тех же объектах), подключение не состоится, и connect вернёт false.
Этот пример иллюстрирует, что объекты могут работать вместе, не имея никакой информации друг о друге. Для этого объекты должны быть соединены, и это можно сделать с помощью нескольких простых вызовов функции QObject::connect() или функции автоматического подключения в uic.
Реальный пример
Следующий пример — заголовок простого класса виджета без методов-членов. Цель показать, как вы можете использовать сигналы и слоты в своих собственных приложениях.
#ifndef LCDNUMBER_H
#define LCDNUMBER_H
#include <QFrame>
class LcdNumber : public QFrame
{
Q_OBJECT LcdNumber наследует от QObject, который имеет большую часть знаний о сигналах и слотах, через QFrame и QWidget. Он чем-то похож на встроенный виджет QLCDNumber.
Макрос Q_OBJECT расширяется препроцессором, чтобы объявить несколько методов-членов, которые реализуются модулем moc; если у вас появляются ошибки компиляции вида "undefined reference to vtable for LcdNumber", вы, вероятно, забыли запустить moc или включить вывод moc в командной строке линковки.
public:
LcdNumber(QWidget *parent = nullptr);
signals:
void overflow(); После конструктора класса и public членов, мы объявляем класс signals. Класс LcdNumber генерирует сигнал overflow(), когда ему задано показать невозможное значение.
Если вам не важен переполнение, или вы знаете, что переполнение не может произойти, вы можете проигнорировать сигнал overflow(), т.е. не подключать его ни к одному слоту.
Если же вы хотите вызвать две разные функции обработки ошибок при переполнении числа, просто подключите сигнал к двум различным слотам. Qt вызовет оба (в порядке их подключения).
public slots:
void display(int num);
void display(double num);
void display(const QString &str);
void setHexMode();
void setDecMode();
void setOctMode();
void setBinMode();
void setSmallDecimalPoint(bool point);
};
#endif Слот — это функция-получатель, используемая для получения информации о изменениях состояния в других виджетах. LcdNumber использует её, как показано в коде выше, для установки отображаемого числа. Поскольку display() является частью интерфейса класса с остальной частью программы, слот является публичным.
Несколько примеров программ подключают сигнал valueChanged() QScrollBar к слоту display(), чтобы число на ЖК-дисплее непрерывно отображало значение ползунка.
Обратите внимание, что display() перегружен; Qt выберет подходящую версию, когда вы подключите сигнал к слоту. С обратными вызовами вам пришлось бы найти пять разных имён и отслеживать типы самостоятельно.
Сигналы и слоты с аргументами по умолчанию
Подписи сигналов и слотов могут содержать аргументы, и аргументы могут иметь значения по умолчанию. Рассмотрим QObject::destroyed():
void destroyed(QObject* = nullptr);
Когда QObject удаляется, он испускает этот сигнал QObject::destroyed(). Мы хотим перехватить этот сигнал, где бы у нас ни была висячая ссылка на удалённый QObject, чтобы мы могли его очистить. Подходящая подпись слота может быть следующей:
void objectDestroyed(QObject* obj = nullptr);
Для подключения сигнала к слоту мы используем QObject::connect(). Есть несколько способов подключения сигнала и слота. Первый — использовать указатели на функции:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
Использование QObject::connect() с указателями на функции имеет несколько преимуществ. Во-первых, это позволяет компилятору проверить, что аргументы сигнала совместимы с аргументами слота. Аргументы также могут быть неявно преобразованы компилятором, если это необходимо.
Вы также можете подключиться к функторам или лямбда-выражениям C++11:
connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); }); В обоих этих случаях мы предоставляем this в качестве контекста в вызове connect(). Объект контекста предоставляет информацию о том, в какой потоке должен быть выполнен получатель. Это важно, поскольку предоставление контекста гарантирует, что получатель выполняется в потоке контекста.
Лямбда-выражение будет отключено, когда отправитель или контекст уничтожаются. Вы должны следить за тем, чтобы все объекты, используемые внутри функтора, оставались живыми, когда сигнал испускается.
Другой способ подключения сигнала к слоту — использовать QObject::connect() и макросы SIGNAL и SLOT. Правило о том, следует ли включать аргументы в макросы SIGNAL() и SLOT(), если аргументы имеют значения по умолчанию, состоит в том, что подпись, переданная макросу SIGNAL(), не должна содержать меньше аргументов, чем подпись, переданная макросу SLOT().
Всё это будет работать:
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*))); connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed())); connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));
Но это не сработает:
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));
…потому что слот будет ожидать QObject, которого сигнал не передаст. Это соединение сообщит об ошибке во время выполнения.
Обратите внимание, что компилятор не проверяет аргументы сигналов и слотов при использовании этого перегрузки QObject::connect().
Расширенное использование сигналов и слотов
В тех случаях, когда вам может потребоваться информация об отправителе сигнала, Qt предоставляет функцию QObject::sender(), которая возвращает указатель на объект, отправивший сигнал.
Лямбда-выражения — удобный способ передачи пользовательских аргументов в слот:
connect(action, &QAction::triggered, engine,
[=]() { engine->processAction(action->text()); }); Использование Qt с сигналами и слотами сторонних разработчиков
Можно использовать Qt с механизмом сигналов/слотов сторонних разработчиков. Вы даже можете использовать оба механизма в одном проекте. Добавьте следующую строку в файл вашего проекта qmake (.pro).
CONFIG += no_keywords
Это говорит Qt не определять ключевые слова moc signals, slots, и emit, потому что эти имена будут использоваться сторонней библиотекой, например, Boost. Затем, чтобы продолжить использование сигналов и слотов Qt с флагом no_keywords, просто замените все используемые ключевые слова moc Qt соответствующими макросами Qt Q_SIGNALS (или Q_SIGNAL), Q_SLOTS (или Q_SLOT) и Q_EMIT.
См. также QLCDNumber, QObject::connect(), Пример Цифровых Часов, Пример Tetrix, Система метаобъектов и Система свойств Qt.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.15/signalsandslots.html