本文整理了一些当前项目中使用的Qt5.6版本信号、槽connect新旧写法的比较、需要注意的问题。
一、connect:string-based和functor-based写法比较
1.1 概述
自Qt 5.0以来,Qt提供了两种C++信号槽connect的写法:string-based和functor-based。
示例代码:
ClassA *pClassA = new ClassA();
Classb *pClassB = new ClassB();
/* functor -based*/
connect(pClassA, &ClassA::signalA, pClassB, &ClassB::slotB);
/* string-based */
connect(pClassA, SIGNAL(signalA(int)), pClassB, SLOT(slotB(int)));
两种connect写法各有优缺点,下表概述了它们的差异:
string-based | functor-based | |
---|---|---|
类型检查时机 | 运行时 | 编译时 |
隐式类型转换 | Yes | |
连接signals和lambda表达式 | Yes | |
连接signals到更多参数的slots(使用默认参数) | Yes | |
连接C++函数和QML函数 | Yes | |
当对象的signal和slot重载时写法 | 简单 | 复杂 |
1.2 类型检查和隐式转换
string-based的类型检查是在运行时做的,因此有以下限制:
- connect错误只有在程序运行起来后才会提示
- 无法在signals和slots之间进行类型隐式转换
- 无法处理typedefs、namespaces
1、2是因为字符串比较无法获取C++类型信息,因此string-based依赖于直接用字符串匹配。
与string-based相反,functor-based通过编译时检查,支持可比较类型的隐式转换,并且可以识别同种类型的不同名称。
示例代码:
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)));
上述例子中,演示了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
class TextSender : public QWidget
{
Q_OBJECT
QLineEdit *lineEdit;
QPushButton *button;
signals:
void textCompleted(const QString& text) const;
public:
TextSender(QWidget *parent = nullptr);
};
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
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!")
}
}
.h(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++!");
});
}
};
.cpp
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。
1.5 连接signals和包含更多参数的slots(使用默认参数)
通常情况下,connect的slot参数数量小于等于signal,且所有参数类型都得是相容的。
示例代码:
/* signal和slot参数数目相同 */
connect(pClassA, SIGNAL(signalA(QString str1, int i1)), pClassB, SLOT(slot(QString str1, int i1)));
/* slot参数比signal少 */
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
public slots:
/* 带默认参数的槽函数 */
void printNumber(int number = 42)
{
qDebug() << "Lucky number" << number;
}
.cpp
DemoWidget::DemoWidget(QWidget *parent) : QWidget(parent)
{
/* OK: printNumber() 会传入默认参数 42 */
connect(qApp, SIGNAL(aboutToQuit()), this, SLOT(printNumber()));
/* 编译报错: 编译器需要相容的参数 */
connect(qApp, &QCoreApplication::aboutToQuit, this, &DemoWidget::printNumber);
}
1.6 连接重载的信号和槽
由于string-based写法要求指明参数类型,因此可以用于连接重载的信号和槽。
例如连接以下信号和槽:
信号:
QSlider::valueChanged()
槽:
QLCDNumber::display(int)
QLCDNumber::display(double)
QLCDNumber::display(QString)
string-based连接写法
示例代码:
auto slider = new QSlider(this);
auto lcd = new QLCDNumber(this);
/* String-based syntax */
connect(slider, SIGNAL(valueChanged(int)), lcd, SLOT(display(int)));
换作functor-based写法,得这样
示例代码:
/* 方法一 */
connect(slider, &QSlider::valueChanged, lcd, static_cast<void (QLCDNumber::*)(int)>(&QLCDNumber::display));
/* 方法二 */
void (QLCDNumber::*mySlot)(int) = &QLCDNumber::display;
connect(slider, &QSlider::valueChanged, lcd, mySlot);
/* 方法三 */
connect(slider, &QSlider::valueChanged, lcd, QOverload<int>::of(&QLCDNumber::display));
/* 方法四 (需要C++14) */
connect(slider, &QSlider::valueChanged, lcd, qOverload<int>(&QLCDNumber::display));
1.7 参考资料
Qt Assistant搜索 Differences between String-Based and Functor-Based Connections
二、connect:第五个参数
Qt :: ConnectionType 是枚举类型,描述了信号和槽的连接类型。它决定了信号是立即传送到槽,还是排队等待稍后传送。
常量 | 值 | 描述 |
---|---|---|
Qt::AutoConnection | 0 | (默认)如果信号是从与接收对象不同的线程发出的,则信号将排队,表现为Qt :: QueuedConnection。否则,直接调用槽函数,表现为Qt :: DirectConnection。发射信号时确定连接类型。 |
Qt::DirectConnection | 1 | 发出信号时直接调用槽函数,即直接函数调用。 |
Qt::QueuedConnection | 2 | 当控制返回到接收者线程的事件循环时,将调用该槽。槽函数在接收者所在的线程中执行。 |
Qt::BlockingQueuedConnection | 4 | 与QueuedConnection类似,但会在当前线程阻塞,直到槽返回。此连接类型仅应在发射者和接收者位于不同线程中的情况下使用。 |
三、信号槽connect应注意的问题
1、有三种情况必须使用 disconnect() 函数
1、断开与某个对象相关联的任何对象。这似乎有点不可理解,事实上,当我们在某个对象中定义了一个或者多个信号,这些信号与另外若干个对象中的槽相关联,如果我们要切断这些关联的话,就可以利用这个方法,非常之简洁。
disconnect( myObject, 0, 0, 0 )
或者
myObject->disconnect()
2、断开与某个特定信号的任何关联。
disconnect( myObject, SIGNAL(mySignal()), 0, 0 )
或者
myObject->disconnect( SIGNAL(mySignal()) )
3、断开两个对象之间的关联。
disconnect( myObject, 0, myReceiver, 0 )
或者
myObject->disconnect( myReceiver )
在disconnect函数中0可以用作一个通配符,分别表示任何信号、任何接收对象、接收对象中的任何槽函数。但是发射者 sender 不能为 0,其它三个参数的值可以等于 0。
2、宏定义不能用在 signal 和 slot 的参数中
既然 moc 工具不扩展 #define,因此,在 signals 和 slots 中携带参数的宏就不能正确地工作,如果不带参数是可以的。例如,下面的例子中将带有参数的宏 SIGNEDNESS(a) 作为信号的参数是不合语法的:
#ifdef ultrix
#define SIGNEDNESS(a) unsigned a
#else
#define SIGNEDNESS(a) a
#endif
class Whatever : public QObject
{
[...]
signals:
void someSignal( SIGNEDNESS(a) );
[...]
};
3、构造函数不能用在 signals 或者 slots 声明区域内
的确,将一个构造函数放在 signals 或者 slots 区内有点不可理解,无论如何,不能将它们放在 private slots、protected slots 或者 public slots 区内。下面的用法是不合语法要求的:
class SomeClass : public QObject
{
Q_OBJECT
public slots:
SomeClass( QObject *parent, const char *name )
: QObject( parent, name ) {} /* 在槽声明区内声明构造函数不合语法 */
};
4、函数指针不能作为信号或槽的参数
例如,下面的例子中将 void (applyFunction)(QList, void*) 作为参数是不合语法的:
class someClass : public QObject
{
Q_OBJECT
public slots:
void apply(void (*applyFunction)(QList*, void*), char*); /* 不合语法 */
};
你可以采用下面的方法绕过这个限制:
typedef void (*ApplyFunctionType)(QList*, void*);
class someClass : public QObject
{
Q_OBJECT
public slots:
void apply( ApplyFunctionType, char *);
};
5、 信号与槽不能有缺省参数
既然signal->slot绑定是发生在运行时刻,那么,从概念上讲使用缺省参数是困难的。下面的用法是不合理的:
class SomeClass : public QObject
{
Q_OBJECT
public slots:
void someSlot(int x=100); /* 将 x 的缺省值定义成 100,在槽函数声明中使用是错误的 */
};
6、信号与槽也不能携带模板类参数
如果将信号、槽声明为模板类参数的话,即使 moc 工具不报告错误,也不可能得到预期的结果。 例如,下面的例子中当信号发射时,槽函数不会被正确调用:
public slots:
void MyWidget::setLocation (pair<int,int> location);
public signals:
void MyObject::moved (pair<int,int> location);
但是,你可以使用 typedef 语句来绕过这个限制。如下所示:
typedef pair<int,int> IntPair;
public slots:
void MyWidget::setLocation (IntPair location);
public signals:
void MyObject::moved (IntPair location);
这样使用的话,你就可以得到正确的结果。
7、嵌套的类不能位于信号或槽区域内,也不能有信号或者槽
例如,下面的例子中,在 class B 中声明槽 b() 是不合语法的,在信号区内声明槽 b() 也是不合语法的。
class A
{
Q_OBJECT
public:
class B
{
public slots:
/* 在嵌套类中声明槽不合语法 */
void b();
};
signals:
class B
{
/* 在信号区内声明嵌套类不合语法 */
void b();
}:
};
8、友元声明不能位于信号或者槽声明区内
相反,它们应该在普通 C++ 的 private、protected 或者 public 区内进行声明。下面的例子是不合语法规范的:
class someClass : public QObject
{
Q_OBJECT
signals: /* 信号定义区 */
friend class ClassTemplate; /* 此处定义不合语法 */
};
9、QT重复connect出现重复调用现象(connect N次,则每次emit都会执行N次SLOT函数)
修改方法:
将
connect(ui.messageButton,SIGNAL(pressed()),this,SLOT(on_messageButton_clicked()));
更改为:
connect(ui.messageButton,SIGNAL(pressed()),this,SLOT(on_messageButton_clicked()),Qt::UniqueConnection);
Qt::UniqueConnection可以防止重复连接。如果当前信号和槽已经连接过了,就不再连接了。
四、Qt VS Tools 信号槽常见问题
1、信号、槽修改不同步运行时connect警告
经常会遇到写完信号、槽绑定后,又因为需求变更修改signal、slot函数。倘若没同步修改connect代码,Qt会输出如下警告。这种情况不处理经常导致一些致命bug。