Сигналы и слоты
Сигналы и слоты используются для взаимодействия между объектами. Механизм сигналов и слотов является центральной функцией Qt и, вероятно, наиболее отличающейся частью от функций других фреймворков. Сигналы и слоты реализованы благодаря метаобъектной системе Qt.
Введение
В программировании графического интерфейса, когда мы изменяем один виджет, мы часто хотим, чтобы другой виджет был уведомлен. Более общо, мы хотим, чтобы объекты любого типа могли взаимодействовать друг с другом. Например, если пользователь нажимает кнопку Закрыть, мы, вероятно, хотим, чтобы была вызвана функция close() окна.
Другие инструментарии достигают такого взаимодействия с помощью обратных вызовов. Обратный вызов — это указатель на функцию, поэтому если вы хотите, чтобы функция обработки уведомляла вас о каком-либо событии, вы передаёте указатель на другую функцию (обратный вызов) функции обработки. Затем функция обработки вызывает обратный вызов при необходимости. Хотя такие успешные фреймворки существуют, обратные вызовы могут быть неинтуитивными и могут иметь проблемы с обеспечением корректности типов аргументов обратного вызова.
Сигналы и слоты
В Qt у нас есть альтернатива технике обратных вызовов: мы используем сигналы и слоты. Сигнал генерируется, когда происходит определённое событие. Виджеты Qt имеют много предварительно определённых сигналов, но мы всегда можем подклассировать виджеты, чтобы добавить собственные сигналы. Слот — это функция, которая вызывается в ответ на определённый сигнал. Виджеты Qt имеют много предварительно определённых слотов, но часто используется подклассирование виджетов и добавление собственных слотов, чтобы обрабатывать интересующие вас сигналы.
Механизм сигналов и слотов является безопасным с точки зрения типов: сигнатура сигнала должна совпадать с сигнатурой принимающего слота. (На самом деле слот может иметь более короткую сигнатуру, чем сигнал, который он получает, так как он может игнорировать дополнительные аргументы.) Поскольку сигнатуры совместимы, компилятор может помочь нам обнаружить несоответствия типов при использовании синтаксиса на основе указателей на функции. Синтаксис SIGNAL и SLOT на основе строк обнаружит несоответствия типов во время выполнения. Сигналы и слоты слабо связаны: класс, который испускает сигнал, не знает и не заботится о том, какие слоты принимают сигнал. Механизм сигналов и слотов Qt гарантирует, что если вы подключите сигнал к слоту, слот будет вызван с параметрами сигнала в нужное время. Сигналы и слоты могут принимать любое количество аргументов любого типа. Они полностью безопасны с точки зрения типов.
Все классы, которые наследуют от QObject или одного из его подклассов (например, QWidget), могут содержать сигналы и слоты. Сигналы испускаются объектами, когда они меняют своё состояние таким образом, который может представлять интерес для других объектов. Это всё, что делает объект для взаимодействия. Он не знает и не заботится о том, получает ли кто-либо сигналы, которые он испускает. Это полное инкапсулирование информации и гарантирует, что объект может использоваться как программный компонент.
Слоты могут использоваться для получения сигналов, но они также являются обычными членами-функциями. Так же как объект не знает, получает ли кто-либо его сигналы, слот не знает, есть ли у него какие-либо подключенные к нему сигналы. Это гарантирует, что с Qt могут быть созданы действительно независимые компоненты.
Вы можете подключить сколько угодно сигналов к одному слоту, и один сигнал может быть подключён к сколько угодно слотам. Даже возможно подключить сигнал непосредственно к другому сигналу. (Это вызовет второй сигнал немедленно всякий раз, когда генерируется первый.)
Вместе сигналы и слоты составляют мощный механизм программирования компонентов.
Сигналы
Сигналы испускаются объектом, когда его внутреннее состояние изменилось каким-либо образом, который может быть интересен клиенту или владельцу объекта. Сигналы — это функции с общедоступным доступом и могут быть вызваны откуда угодно, но мы рекомендуем вызывать их только из класса, который определяет сигнал и его подклассов.
Когда испускается сигнал, подключённые к нему слоты обычно выполняются немедленно, как и обычный вызов функции. Когда это происходит, механизм сигналов и слотов полностью независим от любого цикла событий GUI. Выполнение кода, следующего за 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 объекта 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, просто замените все использования ключевых слов Qt moc в ваших исходниках соответствующими макросами 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.13/signalsandslots.html