Differences between String-Based and Functor-Based Connections
从 Qt 5.0 开始, Qt 提供了两种不同的 信号槽连接 方式: 基于字符串 连接语法和 基于函数对象 连接语法. 两种语法都有优点和缺点. 下表总结了它们的差异.
String-based | Functor-based | |
---|---|---|
类型检测时机 | 运行时 | 编译时 |
是否执行隐式类型转换 | Y | |
是否可以将信号连接到 lambda 表达式 | Y | |
是否可以将信号连接到参数比信号多的槽函数 (使用默认参数) | Y | |
是否可以将 C++ 函数连接到 QML 函数 | Y |
以下部分详细解释了这些差异, 并演示如何使用每种连接语法特有的功能.
Type Checking and Implicit Type Conversions
基于字符串的连接通过在运行时比较字符串进行类型检查. 这种方法存在三个限制:
- 只有在程序开始运行后才能检测到连接错误.
- 信号和槽之间不能进行隐式转换.
- 无法解析类型定义和名称空间.
存在限制 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())); // ...
Making Connections to Lambda Expressions
基于函数对象的连接语法可以将信号连接到 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()); }); // ... }
在这个示例中, 即使 QPushButton::clicked() 和 TextSender::textCompleted()
具有不兼容的参数, lambda 函数也使连接变得简单. 相反, 基于字符串的实现将需要额外的样板代码.
注意: 基于函数对象的连接语法接受指向所有函数的指针, 包括独立函数和常规成员函数. 然而, 为了可读性, 信号应该只连接到槽, lambda 表达式和其他信号.
Connecting C++ Objects to QML Objects
基于字符串的语法可以将 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!"'.
有关让 C++ 对象与 QML 对象交互的其他方法, 参见 Interacting with QML Objects from C++.
Using Default Parameters in Slots to Connect to Signals with Fewer Parameters
通常, 只有当槽函数的参数数量与信号相同 (或更少), 并且所有参数类型兼容时才能建立连接.
基于字符串的连接语法为此规则提供了一种解决方法: 如果槽函数具有默认参数, 则可以从信号中省略这些参数. 当发出信号的参数少于槽函数时, 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); }
要使用基于函数对象的语法解决此限制, 请将信号连接到调用槽函数的 lambda 函数, 参见 Making Connections to Lambda Expressions.
Selecting Overloaded Signals and Slots
使用基于字符串的语法, 显式指定参数类型. 因此, 所需的重载信号的实例是明确的.
相反, 使用基于函数对象的语法, 必须强制转换重载的信号或槽, 告诉编译器要使用哪个实例.
例如, 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().