Сигналы и слоты
Сигналы и слоты используются для взаимодействия между объектами. Механизм сигналов и слотов является центральной особенностью 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'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; если у вас возникнут ошибки компиляции типа «неопределенная ссылка на vtable для 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-6.2/signalsandslots.html