基于字符串与基于函数连接之间的差异
Qt提供了两种在C++中编写信号和槽连接的方法:基于字符串的连接语法和基于函数对象的连接语法。这两种语法各有利弊。下表总结了它们之间的差异。
基于字符串 | 基于函数对象 | |
---|---|---|
类型检查是在... | 运行时 | 编译时 |
可以执行隐式类型转换 | 是 | |
可以将信号连接到lambda表达式 | 是 | |
可以将信号连接到比信号参数更多的槽(使用默认参数) | 是 | |
可以将C++函数连接到QML函数 | 是 |
以下各节将详细解释这些差异,并演示如何使用每个连接语法的独特功能。
类型检查和隐式类型转换
基于字符串的连接在运行时通过比较字符串进行类型检查。这种方法存在三个局限性
- 连接错误只能在程序开始运行之后检测到。
- 无法在信号和槽之间进行隐式转换。
- 无法解析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()); }); // ... }
在这个例子中,即使QPushButton::clicked()和TextSender::textCompleted()
具有不兼容的参数,lambda函数仍然使连接变得简单。相比之下,基于字符串的实现将需要更多的样板代码。
注意:基于函数对象的连接语法接受指向所有函数的指针,包括独立的函数和常规成员函数。然而,为了可读性,应仅将信号连接到槽、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)));
当QPushButton被点击时,控制台打印“QML接收:'从C++发出的Hello!'”。同样,当矩形被点击时,控制台打印“C++接收:'从QML发出的Hello!'”。
有关从C++对象与QML对象交互的其他方法,请参阅从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 connect(slider, &QSlider::valueChanged, lcd, qOverload<int>(&QLCDNumber::display));
另请参阅:qOverload。
© 2024 Qt公司有限公司。本文件中包含的文档贡献均为各自所有者的版权。提供的文档受由自由软件基金会发布的GNU自由文档许可证版本1.3条款许可。Qt及其相关标志为芬兰和/或其他国家的Qt公司商标。所有其他商标均为各自所有者的财产。