Отличия между подключениями на основе строк и на основе функторов
Начиная с Qt 5.0, Qt предлагает два разных способа записи подключений «сигнал-слот» в C++: синтаксис подключения на основе строк и синтаксис подключения на основе функторов. У обоих синтаксисов есть свои преимущества и недостатки. В таблице ниже подведены итоги их различий.
| На основе строк | На основе функторов | |
|---|---|---|
| Проверка типов выполняется в... | Во время выполнения | На этапе компиляции |
| Можно выполнять неявные преобразования типов | Да | |
| Можно подключать сигналы к лямбда-выражениям | Да | |
| Можно подключать сигналы к слотам, которые имеют больше аргументов, чем сигнал (используя параметры по умолчанию) | Да | |
| Можно подключать функции C++ к функциям QML | Да |
В следующих разделах подробно описаны эти различия и показано, как использовать функции, уникальные для каждого синтаксиса подключения.
Проверка типов и неявные преобразования типов
Подключения на основе строк проверяют типы, сравнивая строки во время выполнения. Этот подход имеет три ограничения:
- Ошибки подключения могут быть обнаружены только после запуска программы.
- Неявные преобразования не могут выполняться между сигналами и слотами.
- Псевдонимы типов и пространства имен не могут быть разрешены.
Ограничения 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 received: "Hello from C++!"'. Аналогично, при нажатии на Rectangle в консоли выводится: 'C++ received: "Hello from 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():
QLCDNumber::display(int)QLCDNumber::display(double)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.12/signalsandslots-syntaxes.html