Qt——信号槽连接:基于字符串与基于函数的连接之间的不同
从Qt5.0开始,Qt提供了两种不同的方式进行信号槽的连接:基于 字符串 的连接语法、基于 函数 的连接语法。这两种语法各有利弊,下面对它们的不同点进行总结。
以下几部分详细解释了它们之间的不同,并说明如何使用它们,以及各自的优点。
一、类型检查以及隐式类型转换
基于字符串的连接是在运行时通过字符串比较来进行类型检查,这种方式有3个局限性:
1.只有在程序运行后才能查出连接错误;
2.信号和槽之间不能进行隐式转换;
3.类型定义和名字空间不能被识别。
第2第3个局限存在的原因是,字符串的比较并不会涉及C++的类型信息,因此它严格依赖于字符串的匹配。
相反地,基于函数的连接由编译器进行检查,编译器在编译时捕捉错误,并且允许相容类型的隐式转换,而且它能识别相同类型的不同名称。
例如,信号有一个int类型的参数,而槽函数接受一个double类型的参数,只有基于函数的语法能够将他们连接起来。QSlider有一个int值,而QDubleSpinBox有一个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::Satae”,即使“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发出信号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()); }); // ... }
在这个例子中,lambda函数使得连接变得很简单,即使QPushButton::clicked()和TextSender::textCompleted()有不匹配的参数。相反地,如果使用基于字符串的实现方式,则需要额外的代码。
注意:基于函数的连接语法接受所有函数的指针,包括普通的非成员函数以及成员函数,但为了可读性,信号应该只和槽、lambda表达式和其它信号连接。
三、连接C++对象和QML对象
基于字符串的语法可以连接C++对象和QML对象,但是基于函数的语法无法连接。因为QML类型在运行时识别,它们并不适用于C++编译器。
四、在槽中使用默认参数,与参数更少的信号进行连接
通常,只有在槽的参数小于等于信号的参数时,并且所有参数相匹配时,连接才能建立。
基于字符串的连接语法为这个规则提供了一个变通方案:当信号发出并且它的参数比槽的参数少时,如果槽有默认参数,这些参数可以从信号中省略,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, &QApplication::aboutToQuit, this, &DemoWidget::printNumber); }
五、选择重载信号或者槽
使用基于字符串的语法,参数类型是显式确定的。最后,使用哪个重载信号或槽是不明确的。
相反地,使用基于函数的语法,重载的信号或槽必须被强制转换,告诉编译器使用哪个。
例如,QSignalMapper有4种形式的mapped()信号:
1.QSignalMapper::mapped(int)
2.QSignalMapper::mapped(QString)
3.QSignalMapper::mapped(QWidget*)
4.QSignalMapper::mapped(QObject*)
为了连接int版本的QSpinBox::setValue(),两种语法写法如下:
auto mapper = new QSignalMapper(this); auto spinBox = new QSpinBox(this); // String-based syntax connect(mapper, SIGNAL(mapped(int)), spinBox, SLOT(setValue(int))); // Functor-based syntax, first alternative connect(mapper, static_cast<void (QSignalMapper::*)(int)>(&QSignalMapper::mapped), spinBox, &QSpinBox::setValue); // Functor-based syntax, second alternative void (QSignalMapper::*mySignal)(int) = &QSignalMapper::mapped; connect(mapper, mySignal, spinBox, &QSpinBox::setValue);