基于字符串的连接和基于函数的连接之间的差异
从Qt 5.0起,Qt提供了两种不同的C++信号槽连接方式:基于字符串的连接语法(SIGNAL/SLOT将信号/槽转成一个字符串)和基于函数的连接语法。这两种语法各有优缺点。下表总结了它们之间的区别。
基于字符串 | 基于函数 | |
类型校验的完成在… | 运行时 | 编译时 |
可以履行隐式类型转换 | Y | |
可以将信号连接到 Lambda 表达式 | Y | |
当使用默认参数时,可以将信号连接到具有比信号更多参数的槽。 | Y | |
可以将 C++ 函数连接到 QML 函数 | Y |
以下各节详细解释了这些差异,并演示了如何使用每种连接语法独有的特性。
类型校验和隐式类型转换
基于字符串的连接通过在运行时比较字符串来进行类型检查。这种方法有三个限制:
- 连接错误只能在程序启动后检测到。
- 信号和槽之间不能进行隐式转换。
- 不能解析 typedef 和命名空间。
限制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()));
// ...
使连接到 Lambda 表达式
基于函数对象的连接语法可以将信号连接到 C++11 lambda 表达式,这些表达式实际上是内联槽。这个特性在基于字符串的语法中是不可用的。
在以下示例中,TextSender 类发出一个携带 QString 参数的 textCompleted() 信号。以下是类的声明:
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());
});
// ...
}
在这个例子中,lambda函数使连接变得简单,即使 QPushButton::clicked() 和 TextSender::textCompleted() 的参数是不兼容的。相比之下,基于字符串的实现需要额外的样板代码。
注意: 对于函数对象连接语法,可以接受所有函数的指针,包括独立函数和普通成员函数。但是,为了提高可读性,建议将信号仅连接到槽函数、Lambda表达式和其他信号。
将 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)));
注意: 在 QML 中,所有 JavaScript 函数都接受 var
类型的参数,它在 C++ 中对应 QVariant 类型。
当 QPushButton 被点击时,控制台会打印出 'QML received: “Hello from C++!”。同样,当矩形被点击时,控制台会打印出 'C++ received: “Hello from QML!”。
参见 Interacting with QML Objects from C++,了解让 C++ 对象与 QML 对象交互的其他方法。
使用槽默认参数连接到具有较少参数的信号
通常情况下,只有当槽的参数数量与信号相同(或更少),并且所有参数类型都兼容时,才能建立连接。
基于字符串的连接语法提供了一个解决方案:如果槽具有默认参数,则可以从信号中省略这些参数。当信号发射时参数少于槽时,Qt 将使用默认参数值运行槽。
函数对象连接不支持此功能。
假设有一个名为 DemoWidget
的类,其中有一个带有默认参数的槽 printNumber()
:
public slots:
void printNumber(int number = 42) {
qDebug() << "Lucky number" << number;
}
使用基于字符串的连接,即使 QApplication::aboutToQuit() 没有参数,也可以将 DemoWidget::printNumber() 连接到它。而基于函数对象的连接会产生编译时错误。
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);
}
为了解决函数对象语法的这种限制,可以将信号连接到一个调用槽的 lambda 函数。请参见上面的章节(使连接到 Lambda 表达式)。
选择重载的信号和槽
使用基于字符串的语法,参数类型被明确指定。因此,对于重载的信号或槽,所需的实例是明确的。
相比之下,使用函数对象语法,必须对重载的信号或槽进行转换,以告诉编译器使用哪个实例。
例如,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));