Spec-Zone.ru › Qt 5.11

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

Сигналы и слоты используются для обмена сообщениями между объектами. Механизм сигналов и слотов является центральным элементом 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; если у вас появляются ошибки компиляции вида "undefined reference to vtable for LcdNumber", вы, вероятно, забыли запустить moc или включить вывод moc в командной строке линковки.

public:
    LcdNumber(QWidget *parent = 0);

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 выберет соответствующую версию, когда вы подключите сигнал к слоту. При использовании обратных вызовов вам пришлось бы найти пять различных имён и самостоятельно отслеживать типы.

{Пример Tetrix}.

Сигналы и слоты с аргументами по умолчанию

Подписи сигналов и слотов могут содержать аргументы, и эти аргументы могут иметь значения по умолчанию. Рассмотрим 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, [=](){ 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(), Пример цифровых часов, и Систему метаобъектов, и Систему свойств Qt.

© The Qt Company Ltd
Licensed under the GNU Free Documentation License, Version 1.3.
https://doc.qt.io/archives/qt-5.11/signalsandslots.html

Spec-Zone.ru

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