Сигналы и слоты
Сигналы и слоты используются для связи между объектами. Механизм сигналов и слотов является центральной функцией 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; если вы получаете ошибки компилятора типа "неопределённая ссылка на таблицу виртуальных функций для LcdNumber", вы, вероятно, забыли запустить moc или включить вывод moc в команду линковки.
public:
LcdNumber(QWidget *parent = 0); Это не очевидно важно для moc, но если вы наследуете от QWidget, вам почти наверняка понадобится аргумент parent в вашем конструкторе и передать его в конструктор базового класса.
Некоторые деструкторы и функции-члены опущены здесь; moc игнорирует функции-члены.
signals:
void overflow(); LcdNumber испускает сигнал, когда ему сообщается о невозможном значении.
Если вас не волнует переполнение, или вы знаете, что переполнение невозможно, вы можете игнорировать сигнал 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* = 0);
Когда QObject удаляется, он испускает этот сигнал QObject::destroyed(). Мы хотим перехватить этот сигнал, где бы у нас ни было висячая ссылка на удалённый QObject, чтобы очистить её. Подходящая подпись слота может быть следующей:
void objectDestroyed(QObject* obj = 0);
Чтобы подключить сигнал к слоту, используем QObject::connect(). Есть несколько способов подключить сигнал и слот. Первый — использовать указатели на функции:
connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);
Использование QObject::connect() с указателями на функции имеет несколько преимуществ. Во-первых, это позволяет компилятору проверить, что аргументы сигнала совместимы с аргументами слота. Аргументы также могут быть неявным образом преобразованы компилятором, если это необходимо.
Вы также можете подключиться к функторам или лямбда-выражениям C++11:
connect(sender, &QObject::destroyed, [=](){ this->m_objects.remove(sender); }); Другой способ подключить сигнал к слоту — использовать 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(), которая возвращает указатель на объект, отправивший сигнал.
Класс QSignalMapper предоставляется для ситуаций, когда множество сигналов подключены к одному слоту, и слоту нужно обращаться с каждым сигналом по-разному.
Предположим, у вас есть три кнопки, которые определяют, какой файл будет открыт: «Файл налогов», «Файл счетов» или «Файл отчёта».
Чтобы открыть правильный файл, используйте QSignalMapper::setMapping() для сопоставления всех сигналов QPushButton::clicked() с объектом QSignalMapper. Затем подключите сигнал QPushButton::clicked() файла к слоту QSignalMapper::map().
signalMapper = new QSignalMapper(this);
signalMapper->setMapping(taxFileButton, QString("taxfile.txt"));
signalMapper->setMapping(accountFileButton, QString("accountsfile.txt"));
signalMapper->setMapping(reportFileButton, QString("reportfile.txt"));
connect(taxFileButton, &QPushButton::clicked,
signalMapper, &QSignalMapper::map);
connect(accountFileButton, &QPushButton::clicked,
signalMapper, &QSignalMapper::map);
connect(reportFileButton, &QPushButton::clicked,
signalMapper, &QSignalMapper::map); Затем подключите сигнал mapped() к readFile(), где будет открыт другой файл в зависимости от нажатой кнопки.
connect(signalMapper, SIGNAL(mapped(QString)),
this, SLOT(readFile(QString))); Использование Qt с сигналами и слотами сторонних библиотек
Можно использовать Qt с механизмом сигналов/слотов сторонних библиотек. Вы даже можете использовать оба механизма в одном проекте. Просто добавьте следующую строку в файл проекта qmake (.pro):
CONFIG += no_keywords
Это говорит Qt не определять ключевые слова moc signals, slots, и emit, потому что эти имена будут использоваться сторонней библиотекой, например, Boost. Затем, чтобы продолжить использование Qt сигналов и слотов с флагом no_keywords, просто замените все использования ключевых слов Qt moc в ваших исходных файлах соответствующими макросами Qt Q_SIGNALS (или Q_SIGNAL), Q_SLOTS (или Q_SLOT) и Q_EMIT.
См. также Метаобъектную систему и Систему свойств Qt.
© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/qt-5.9/signalsandslots.html