前言
 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对象进入事件循环。
 
文章评论