Spec-Zone.ru › Qt 5.9

Различия между строковыми и функторными подключениями

Начиная с Qt 5.0, Qt предлагает два различных способа написания подключений «сигнал-слот» в C++: синтаксис на основе строк и синтаксис на основе функторов. У обоих синтаксисов есть свои преимущества и недостатки. В таблице ниже подытожены их различия.

Строковый Функторный
Проверка типов выполняется... Во время выполнения На этапе компиляции
Можно выполнять неявные преобразования типов Да
Можно подключать сигналы к лямбда-выражениям Да
Можно подключать сигналы к слотам, имеющим больше аргументов, чем сигнал (с использованием параметров по умолчанию) Да
Можно подключать функции C++ к функциям QML Да
Выбор экземпляра перегруженного сигнала или слота... Простой Сложный

В следующих разделах подробно объясняются эти различия и демонстрируется, как использовать возможности, уникальные для каждого синтаксиса подключения.

Проверка типов и неявные преобразования типов

Строковые подключения проверяют типы, сравнивая строки во время выполнения. Такой подход имеет три ограничения:

  1. Ошибки подключения могут быть обнаружены только после запуска программы.
  2. Неявные преобразования не могут выполняться между сигналами и слотами.
  3. Невозможно разрешить псевдонимы типов и пространства имён.

Ограничения 2 и 3 существуют, потому что сравниватель строк не имеет доступа к информации о типах C++, поэтому он полагается на точное соответствие строк.

В отличие от этого, функторные подключения проверяются компилятором. Компилятор обнаруживает ошибки на этапе компиляции, позволяет выполнять неявные преобразования между совместимыми типами и распознаёт различные имена одного и того же типа.

Например, только функторный синтаксис может быть использован для подключения сигнала, передающего int к слоту, принимающему double. QSlider содержит значение int, а QDoubleSpinBox содержит значение double. Следующий фрагмент кода показывает, как их синхронизировать:

    auto slider = new QSlider(this);
    auto doubleSpinBox = new QDoubleSpinBox(this);

    // OK: The compiler can convert an int into a double
    connect(slider, &QSlider::valueChanged,
            doubleSpinBox, &QDoubleSpinBox::setValue);

    // ERROR: The string table doesn't contain conversion information
    connect(slider, SIGNAL(valueChanged(int)),
            doubleSpinBox, SLOT(setValue(double)));

Следующий пример иллюстрирует отсутствие разрешения имен. QAudioInput::stateChanged() объявляется с аргументом типа "QAudio::State". Таким образом, строковые подключения должны также указывать "QAudio::State", даже если "State" уже видно. Эта проблема не относится к функторным подключениям, поскольку типы аргументов не являются частью подключения.

    auto audioInput = new QAudioInput(QAudioFormat(), this);
    auto widget = new QWidget(this);

    // OK
    connect(audioInput, SIGNAL(stateChanged(QAudio::State)),
            widget, SLOT(show()));

    // ERROR: The strings "State" and "QAudio::State" don't match
    using namespace QAudio;
    connect(audioInput, SIGNAL(stateChanged(State)),
            widget, SLOT(show()));

    // ...

Подключение к лямбда-выражениям

Синтаксис подключения на основе функторов позволяет подключать сигналы к лямбда-выражениям C++11, которые являются эффективными встроенными слотами. Эта функция недоступна со строковым синтаксисом.

В следующем примере класс TextSender испускает сигнал textCompleted(), который несёт параметр QString. Вот объявление класса:

class TextSender : public QWidget {
    Q_OBJECT

    QLineEdit *lineEdit;
    QPushButton *button;

signals:
    void textCompleted(const QString& text) const;

public:
    TextSender(QWidget *parent = nullptr);
};

Вот подключение, которое испускает TextSender::textCompleted(), когда пользователь нажимает на кнопку:

TextSender::TextSender(QWidget *parent) : QWidget(parent) {
    lineEdit = new QLineEdit(this);
    button = new QPushButton("Send", this);

    connect(button, &QPushButton::clicked, [=] {
        emit textCompleted(lineEdit->text());
    });

    // ...
}

В этом примере лямбда-функция упростила подключение, несмотря на то, что QPushButton::clicked() и TextSender::textCompleted() имеют несовместимые параметры. В отличие от этого, строковая реализация потребовала бы дополнительный код.

Примечание: Функторный синтаксис подключения принимает указатели на все функции, включая отдельные функции и обычные функции-члены. Однако для лучшей читаемости сигналы должны подключаться только к слотам, лямбда-выражениям и другим сигналам.

Подключение объектов C++ к объектам QML

Строковый синтаксис может подключать объекты C++ к объектам QML, но функторный синтаксис не может. Это связано с тем, что типы QML разрешаются во время выполнения, поэтому они недоступны компилятору C++.

В следующем примере щелчок по объекту QML заставляет объект C++ выводить сообщение, и наоборот. Вот тип QML (в QmlGui.qml):

Rectangle {
    width: 100; height: 100

    signal qmlSignal(string sentMsg)
    function qmlSlot(receivedMsg) {
        console.log("QML received: " + receivedMsg)
    }

    MouseArea {
        anchors.fill: parent
        onClicked: qmlSignal("Hello from QML!")
    }
}

Вот класс C++:

class CppGui : public QWidget {
    Q_OBJECT

    QPushButton *button;

signals:
    void cppSignal(const QVariant& sentMsg) const;

public slots:
    void cppSlot(const QString& receivedMsg) const {
        qDebug() << "C++ received:" << receivedMsg;
    }

public:
    CppGui(QWidget *parent = nullptr) : QWidget(parent) {
        button = new QPushButton("Click Me!", this);
        connect(button, &QPushButton::clicked, [=] {
            emit cppSignal("Hello from C++!");
        });
    }
};

Вот код, который устанавливает подключения сигнал-слот:

    auto cppObj = new CppGui(this);
    auto quickWidget = new QQuickWidget(QUrl("QmlGui.qml"), this);
    auto qmlObj = quickWidget->rootObject();

    // Connect QML signal to C++ slot
    connect(qmlObj, SIGNAL(qmlSignal(QString)),
            cppObj, SLOT(cppSlot(QString)));

    // Connect C++ signal to QML slot
    connect(cppObj, SIGNAL(cppSignal(QVariant)),
            qmlObj, SLOT(qmlSlot(QVariant)));

Примечание: Все функции JavaScript в QML принимают параметры типа var, который сопоставляется с типом QVariant в C++.

При нажатии на QPushButton в консоль выводится «QML получило: "Привет из C++!"». Аналогично, при нажатии на Rectangle в консоль выводится «C++ получило: "Привет из QML!"».

См. Взаимодействие с объектами QML из C++ для других способов взаимодействия объектов C++ с объектами QML.

Использование параметров по умолчанию в слотах для подключения к сигналам с меньшим количеством параметров

Обычно подключение может быть выполнено только в том случае, если у слота такое же количество аргументов, как у сигнала (или меньше), и если все типы аргументов совместимы.

Строковый синтаксис подключения предоставляет обходной путь для этого правила: если у слота есть параметры по умолчанию, эти параметры могут быть опущены из сигнала. Когда сигнал испускается с меньшим количеством аргументов, чем слот, Qt выполняет слот, используя значения параметров по умолчанию.

Функторные подключения не поддерживают эту функцию.

Предположим, есть класс под названием DemoWidget со слотом printNumber(), у которого есть аргумент по умолчанию:

public slots:
    void printNumber(int number = 42) {
        qDebug() << "Lucky number" << number;
    }

Используя строковое подключение, DemoWidget::printNumber() может быть подключено к QApplication::aboutToQuit(), несмотря на то, что последний не имеет аргументов. Функторное подключение вызовет ошибку компиляции:

DemoWidget::DemoWidget(QWidget *parent) : QWidget(parent) {

    // OK: printNumber() will be called with a default value of 42
    connect(qApp, SIGNAL(aboutToQuit()),
            this, SLOT(printNumber()));

    // ERROR: Compiler requires compatible arguments
    connect(qApp, &QCoreApplication::aboutToQuit,
            this, &DemoWidget::printNumber);
}

Чтобы обойти это ограничение с функторным синтаксисом, подключите сигнал к лямбда-функции, которая вызывает слот. См. раздел выше, Подключение к лямбда-выражениям.

Выбор перегруженных сигналов и слотов

В строковом синтаксисе типы параметров явно указываются. В результате, желаемый экземпляр перегруженного сигнала или слота однозначен.

В отличие от этого, в функторном синтаксисе перегруженный сигнал или слот необходимо привести к типу, чтобы указать компилятору, какой экземпляр использовать.

Например, у QLCDNumber есть три версии слота display():

  1. QLCDNumber::display(int)
  2. QLCDNumber::display(double)
  3. QLCDNumber::display(QString)

Для подключения версии int к QSlider::valueChanged() оба синтаксиса будут:

    auto slider = new QSlider(this);
    auto lcd = new QLCDNumber(this);

    // String-based syntax
    connect(slider, SIGNAL(valueChanged(int)),
            lcd, SLOT(display(int)));

    // Functor-based syntax, first alternative
    connect(slider, &QSlider::valueChanged,
            lcd, static_cast<void (QLCDNumber::*)(int)>(&QLCDNumber::display));

    // Functor-based syntax, second alternative
    void (QLCDNumber::*mySlot)(int) = &QLCDNumber::display;
    connect(slider, &QSlider::valueChanged,
            lcd, mySlot);

    // Functor-based syntax, third alternative
    connect(slider, &QSlider::valueChanged,
            lcd, QOverload<int>::of(&QLCDNumber::display));

    // Functor-based syntax, fourth alternative (requires C++14)
    connect(slider, &QSlider::valueChanged,
            lcd, qOverload<int>(&QLCDNumber::display));

См. также qOverload().

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

Spec-Zone.ru

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