Qt信号槽技术小结
一、connect:String-based和Functor-based写法比较
1.1 概述
从Qt 5.0开始,Qt、C++支持两种信号槽connect写法:string-based、functor-based。
示例代码:
1 ClassA *pClassA = new ClassA(); 2 Classb *pClassB = new ClassB(); 3 4 /* string-based */ 5 connect(pClassA, SIGNAL(signalA(int)), pClassB, SLOT(slotB(int)));
6
7 /* functor -based*/
8 connect(pClassA, &ClassA::signalA, pClassB, &ClassB::slotB);
下面列表概述了两者的优缺点:
String-based | Functor-based | |
类型检查时机 | 运行时 | 编译时 |
隐式类型转换 | Yes | |
连接signals和lambda表达式 | Yes | |
连接signals和包含更多参数的slots(使用默认参数) | Yes | |
连接C++函数和QML函数 | Yes |
1.2 类型检查、隐式转换
string-based连接通过运行时比较字符串作类型检查,因此有以下缺点:
- 连接错误只能在程序开始运行后提示;
- 信号槽之间无法作隐式类型转换;
- 无法处理typdefs和namespaces。
1、2是因为字符串比较无法获取C++类型信息,因此string-based依赖于直接用字符串匹配。
与string-based相反,functor-based通过编译时检查,支持可比较类型的隐式转换,并且可以识别同种类型的不同名称。
示例代码:
1 auto slider = new QSlider(this); 2 auto doubleSpinBox = new QDoubleSpinBox(this); 3 4 /* OK: The compiler can convert an int into a double */ 5 connect(slider, &QSlider::valueChanged, doubleSpinBox, &QDoubleSpinBox::setValue); 6 7 /* ERROR: The string table doesn't contain conversion information */ 8 connect(slider, SIGNAL(valueChanged(int)), doubleSpinBox, SLOT(setValue(double)));
上述例子中,演示了functor-based连接了参数为int的信号和参数为double的槽函数。
注意:string-based信号槽类型不匹配connect不起效。应用程序输出中可以看到以下错误提示:
QObject::connect: Incompatible sender/receiver arguments
QSlider::valueChanged(int) --> QDoubleSpinBox::setValue(double)
下面代码说明string-based连接无法处理同一个类型用不同名称表示的情况。比如,
QAudioInput::stateChanged()声明的时候参数类型是“QAudio::State”,string-based连接要求connect时必须指定“QAudio::State”,而不能是“State”。functor-based连接由于连接时无需指定参数类型,因此不存在这种问题。
示例代码:
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()));
1.3 连接lambda表达式
functor-based写法支持C++11的lambda表达式,可以写出高效、内联的槽函数。
string-based写法不支持上述特性。
下面以一个名叫TextSender的类为例。
示例代码:
TextSender.h
1 class TextSender : public QWidget 2 { 3 Q_OBJECT 4 5 QLineEdit *lineEdit; 6 QPushButton *button; 7 8 signals: 9 void textCompleted(const QString& text) const; 10 11 public: 12 TextSender(QWidget *parent = nullptr); 13 };
TextSender.cpp
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表达式就可以相对容易地“connect”两者。
注意:虽然functor-based接收所有指向函数的指针,但是Qt中signals只能connect到slots、lambda表达式和其它signals。
1.4 连接C++对象和QML对象
因为QML类型是运行时处理的,而非在C++编译时,所以无法应用到functor-based连接。
下面演示了点击QML对象(C++对象),使得C++对象(QML对象)打印消息。
示例代码:
QmlGui.qml
1 Rectangle { 2 width: 100; height: 100 3 4 signal qmlSignal(string sentMsg) 5 function qmlSlot(receivedMsg) { 6 console.log("QML received: " + receivedMsg) 7 } 8 9 MouseArea { 10 anchors.fill: parent 11 onClicked: qmlSignal("Hello from QML!") 12 } 13 }
.h(C++)
1 class CppGui : public QWidget { 2 Q_OBJECT 3 4 QPushButton *button; 5 6 signals: 7 void cppSignal(const QVariant& sentMsg) const; 8 9 public slots: 10 void cppSlot(const QString& receivedMsg) const { 11 qDebug() << "C++ received:" << receivedMsg; 12 } 13 14 public: 15 CppGui(QWidget *parent = nullptr) : QWidget(parent) { 16 button = new QPushButton("Click Me!", this); 17 connect(button, &QPushButton::clicked, [=] { 18 emit cppSignal("Hello from C++!"); 19 }); 20 } 21 };
.cpp
1 auto cppObj = new CppGui(this); 2 auto quickWidget = new QQuickWidget(QUrl("QmlGui.qml"), this); 3 auto qmlObj = quickWidget->rootObject(); 4 5 /* Connect QML signal to C++ slot */ 6 connect(qmlObj, SIGNAL(qmlSignal(QString)), cppObj, SLOT(cppSlot(QString))); 7 8 /* Connect C++ signal to QML slot */ 9 connect(cppObj, SIGNAL(cppSignal(QVariant)), qmlObj, SLOT(qmlSlot(QVariant)));
QML中的所有JavaScript函数的参数类型都是var,对应C++中的QVariant。
1.5 连接signals和包含更多参数的slots(使用默认参数)
通常情况下,connect的slot参数数量小于等于signal,且所有参数类型都得是相容的。
示例代码:
1 /* signal和slot参数数目相同 */ 2 connect(pClassA, SIGNAL(signalA(QString str1, int i1)), pClassB, SLOT(slot(QString str1, int i1))); 3 4 /* slot参数比signal少 */ 5 connect(pClassA, SIGNAL(signalA(QString str1, int i1)), pClassB, SLOT(slot(QString str1)));
注意:slot参数比signal少,必须是后边的参数缺省,不能是前面的或者中间的;string-based要求的参数匹配,必须类型完全一致,若不一致,即使是QVariant也不行,否则都会运行时提示连接错误,不生效。
string-based连接写法支持一种场景:
当slot有默认参数时,signal可以省略这些默认参数;
当emit省略部分参数的signal时,slot会用默认参数代替省略部分。
相反,functor-based写法不支持上述场,不过functor-based可以通过lambda表达式实现相同的效果。
.h
1 public slots: 2 /* 带默认参数的槽函数 */ 3 void printNumber(int number = 42) 4 { 5 qDebug() << "Lucky number" << number; 6 }
.cpp
1 DemoWidget::DemoWidget(QWidget *parent) : QWidget(parent) 2 { 3 4 /* OK: printNumber() 会传入默认参数 42 */ 5 connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(printNumber())); 6 7 /* 编译报错: 编译器需要相容的参数 */ 8 connect(qApp, &QCoreApplication::aboutToQuit, this, &DemoWidget::printNumber); 9 }
1.6 连接重载的信号和槽
由于string-based写法要求指明参数类型,因此可以用于连接重载的信号和槽。
例如连接以下信号和槽:
信号:
QSlider::valueChanged()
槽:
QLCDNumber::display(int) QLCDNumber::display(double) QLCDNumber::display(QString)
string-based连接写法:
1 auto slider = new QSlider(this); 2 auto lcd = new QLCDNumber(this); 3 4 /* String-based syntax */ 5 connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)));
换作functor-based写法,得这样:
1 /* 方法一 */ 2 connect(slider, &QSlider::valueChanged, lcd, static_cast<void (QLCDNumber::*)(int)>(&QLCDNumber::display)); 3 4 /* 方法二 */ 5 void (QLCDNumber::*mySlot)(int) = &QLCDNumber::display; 6 connect(slider, &QSlider::valueChanged, lcd, mySlot); 7 8 /* 方法三 */ 9 connect(slider, &QSlider::valueChanged, lcd, QOverload<int>::of(&QLCDNumber::display)); 10 11 /* 方法四 (需要C++14) */ 12 connect(slider, &QSlider::valueChanged, lcd, qOverload<int>(&QLCDNumber::display));
1.7 参考资料
Qt Assistant搜索 Differences between String-Based and Functor-Based Connections