Qt——信号槽连接:基于字符串与基于函数的连接之间的不同 一、类型检查以及隐式类型转换 二、使用Lambda表达式进行连接 三、连接C++对象和QML对象 四、在槽中使用默认参数,与参数更少的信号进行连接 五、选择重载信号或者槽

从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);