前言
QT中的信号(signal)与槽(slot)是非常好用的机制,简单来说就是声明两个函数,声明时使用特殊宏将函数声明为信号函数和槽函数,之后再使用connect()函数,将两个函数绑定在一起。做好了这些工作之后,调用信号函数就会触发槽函数的调用,又根据绑定时所传递的参数不同,分为直接触发、延迟触发,跨线程触发等。
信号与槽函数的简单使用
不废话,直接上代码
class SignalSlotTest:public QObject{ Q_OBJECT public: SignalSlotTest(QObject* parent = nullptrA); ~SignalSlotTest(); void emitSignal(); signals: //声明信号 void thisIsSignal(QString); public slots: //声明槽函数 void thisIsSlot(QString str); }; SignalSlotTest::SignalSlotTest(QObject *parent) :QObject(parent) { //绑定信号与槽 connect(this,SIGNAL(thisIsSignal(QString)),SLOT(thisIsSlot(QString))); } SignalSlotTest::~SignalSlotTest() { } void SignalSlotTest::emitSignal() { emit thisIsSignal("First signal!"); } void SignalSlotTest::thisIsSlot(QString str) { qDebug() << str; } int main(int argc, char *argv[]) { QApplication a(argc, argv); SignalSlotTest signalSlotTest; signalSlotTest.emitSignal(); return a.exec(); }
运行结果:
上述代码示范了信号与槽函数最简单的使用方法,接下来会从几个方面来介绍这个代码。
使用信号与槽函数的基本条件
以下是基本条件:
- 拥有信号函数或者槽函数的类,头文件中必须使用Q_OBJECT宏,这个宏会在类中声明一些信号槽机制需要用到的变量以及成员函数,在qmake或者cmake阶段,会根据这个宏,在moc文件中生成对应的调用代码(前提是在qmake或者cmake中添加相关的代码)。
- 拥有信号函数或者槽函数的类,必须直接或者间接的继承QObject类。
- 程序中需要有QApplication 或者 QCoreApplication 对象,最后有在执行事件循环,否则会有一丢丢的问题,现在先暂时不表。
- 信号和槽函数必须使用connect函数绑定。
参数
信号跟槽函数可以有参数,也可以没有,但是相互绑定的信号跟槽函数应该保持一样的参数类型与个数。
绑定
信号与槽函数使用QMetaObject::Connection QObject::connect(const QObject sender*, const char *signal, const QObject receiver*, const char *method, Qt::ConnectionType type = Qt::AutoConnection)进行绑定。
connect函数是QObject的一个静态函数,如果已经继承了QObject的类中可以直接使用,如果是在其他类中绑定,那么使用QObject::connect() 就可以了。
这个函数一共有五个参数:
- 信号发送者
- 信号函数
- 信号接收者
- 槽函数
- 绑定类型
第五个参数是有默认值的,可以选择不传。
在QT5之后,QT官方推荐使用上述代码中的方式来进行信号跟槽的绑定,即使用SIGNAL和SLOT宏来包裹信号和槽函数,相比之前的绑定方式,可以在编译期检查函数参数的个数和类型是否对应。
connect(this,SIGNAL(thisIsSignal(QString)),SLOT(thisIsSlot(QString)));
就像是这样。
访问权限
信号函数的权限是public的,这个我们可以看signals的宏定义:
#define slots Q_SLOTS # define Q_SIGNALS public QT_ANNOTATE_ACCESS_SPECIFIER(qt_signal)
当然,在不同的QT版本可能会有点不一样,这个可以自行查看。
因为是public权限的,所以是可以在当做成员函数直接调用的,调用时就会触发与之绑定的槽函数。
槽函数的声明时可以自己设置权限,这个权限不会影响槽函数是否能被信号触发,只影响用户是否可以在对象外部调用。
小结
这样单独看信号与槽的使用,有点脱裤子放屁的感觉,这样和直接调用有什么区别呢?确实,至少从上述代码看起来是这样的。
但是QT自己的库中,提供了相当多的信号与槽,这样让开发者不用写烦人的回调函数,也可以实现类似的效果。
比如按钮被按下的信号,输入框输入的信号,这个机制使得QT使用起来非常的方便,至少我是这样认为的。
并且信号与槽是可以跨线程触发的,yysd!!
绑定参数
Constant | Value | 描述 |
---|---|---|
Qt::AutoConnection |
0 |
(默认参数)如果接收方是发出信号的线程,则使用Qt::D irectConnection。否则,将使用 Qt::QueuedConnection。连接类型在发出信号时确定。 |
Qt::DirectConnection |
1 |
发出信号时,立即触发槽函数,槽函数在发出信号的线程中执行 |
Qt::QueuedConnection |
2 |
信号被触发时,将槽函数放入事件循环列队,在事件循环列队中调用这个函数。 |
Qt::BlockingQueuedConnection |
3 |
与 Qt::QueuedConnection 相同,不同之处在于信号会使线程阻塞,直到槽函数返回。如果信号和槽函数处于同一个线程,那么将会造成死锁,则导致不能正常使用。 |
Qt::UniqueConnection |
0x80 |
这是一个标志,可以使用按位 OR 与上述任何一种连接类型结合使用。当设置Qt::UniqueConnection时,如果连接已经存在(即,如果同一信号已经连接到同一对对象的同一槽函数),则connect将失败。此标志是在Qt 4.6中引入的。 |
Qt::SingleShotConnection |
0x100 |
这是一个标志,可以使用按位 OR 与上述任何一种连接类型结合使用。当设置Qt::SingleShotConnection时,该槽函数将只被调用一次;发出信号时,连接将自动断开。此标志是在Qt 6.0中引入的。 |
信号与槽的调用机制
调用的机制会根据参数参数的不同而不同,但是有些细节官方文档的并没有写得很清楚,这让我在最近的一个问题中折腾了两三天,最为一个老QT人,真的有点丢人。。
首先,最开始抛出的那一段代码,将main()函数中的代码修改为:
int main(int argc, char *argv[]) { //QApplication a(argc, argv); SignalSlotTest signalSlotTest; signalSlotTest.emitSignal(); return 0;//a.exec(); }
也是可以正常触发槽函数的,因为我们在绑定的时候使用了默认参数,而我们的对象创建的线程(这个很重要)
和触发信号的线程是同一个线程,会被认为信号被触发后立即调用槽函数,这样的情况下,就算没有QApplication或者QCoreApplication都是没有问题的。
如上述,那么将绑定函数改为:
connect(this,SIGNAL(thisIsSignal(QString)),SLOT(thisIsSlot(QString)),Qt::QueuedConnection);
这样再运行程序,就触发不了槽函数了,因为槽函数被放进了事件循环,而我们并没有创建App对象来处理事件循环。需要注意的是App->exec();就是让App对象去处理事件循环,知道程序收到退出的信号,才会返回。也可以在手动的调用void processEvents(QEventLoop::ProcessEventsFlags flags = QEventLoop::AllEvents);函数处理进入循环的事件。
跨线程触发的机制
我一直都认为,进入列队槽函数,触发的线程是主线程(实际上官方文档上也是这样写的),并且在我的实际实践中也是这样,所以我一直没有怀疑这件事情。
当然,这里指的主线程,并不是程序的主线程,而是QApplication对象被创建的线程,它的exec()函数也只能在这个线程里被调用。
将最开始的代码修改一下,在子线程中调用信号,并且打印出子线程和主线程的线程ID:
SignalSlotTest::SignalSlotTest(QObject *parent) :QObject(parent) { connect(this,SIGNAL(thisIsSignal(QString)),SLOT(thisIsSlot(QString)),Qt::QueuedConnection); } void SignalSlotTest::thisIsSlot(QString str) { qDebug() <<"Slot trigger thread ID: " <<QThread::currentThreadId(); qDebug() << str; } int main(int argc, char *argv[]) { QApplication a(argc, argv); qDebug() <<"main thread ID: " <<QThread::currentThreadId(); SignalSlotTest signalSlotTest; QtConcurrent::run([&]{ qDebug() <<"sub thread ID: " <<QThread::currentThreadId(); signalSlotTest.emitSignal(); }); return a.exec(); }
运行结果:
可以看到,槽函数被触发的线程与主线程一致,这与我一惯的认知是相同的。
然后稍微修改一下main()函数中的代码:
int main(int argc, char *argv[]) { QApplication a(argc, argv); qDebug() <<"main thread ID: " <<QThread::currentThreadId(); QtConcurrent::run([&]{ SignalSlotTest signalSlotTest; qDebug() <<"sub thread ID: " <<QThread::currentThreadId(); signalSlotTest.emitSignal(); }); return a.exec(); }
运行结果:
槽函数不会被触发了?
但是只是将对象创建的位置修改了一下,为什么会导致槽函数不会被触发了呢?
再次修改代码:
int main(int argc, char *argv[]) { QApplication a(argc, argv); qDebug() <<"main thread ID: " <<QThread::currentThreadId(); QtConcurrent::run([&]{ SignalSlotTest signalSlotTest; qDebug() <<"sub thread ID: " <<QThread::currentThreadId(); signalSlotTest.emitSignal(); a.processEvents(); }); return a.exec(); }
运行结果:
在子线程中手动调用了事件循环,然后槽函数被触发了,并且它被触发的线程ID是子线程的。
关于跨线程调用的结论
- QT文档中指的将槽函数放入列队,并才槽函数对应的线程被事件循环触发。这里的线程指的是对象被创建的线程。
- 如果对象创建的线程没有事件循环,也就是不在App对象所在的线程,那么槽函数永远不会被触发,除非在这个线程里使得App对象进入事件循环。
文章评论