Сигналы и слоты
Сигналы и слоты используются для связи между объектами. Механизм сигналов и слотов является центральной функцией 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); }); Обратите внимание, что если ваш компилятор не поддерживает C++11 вариативные шаблоны, этот синтаксис работает только в том случае, если сигнал и слот имеют 6 или меньше аргументов.
Другой способ подключить сигнал к слоту — использовать 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/archives/qt-5.6/signalsandslots.html