Spec-Zone.ru › Qt 6.0

Сигналы и слоты

Сигналы и слоты используются для взаимодействия между объектами. Механизм сигналов и слотов является ключевой особенностью 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 в качестве type, подключение будет выполнено только в том случае, если это не дубликат. Если уже существует дубликат (тот же самый сигнал к тому же самому слоту в тех же самых объектах), подключение завершится неудачно, и 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, просто замените все используемые в ваших исходных файлах ключевые слова 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-6.0/signalsandslots.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API