QT 信号(signal)与槽(slot)机制的使用方法

2023年6月16日 4034点热度 0人点赞 0条评论

前言

​ 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();
}

​ 运行结果:

image-20230616205657403

​ 上述代码示范了信号与槽函数最简单的使用方法,接下来会从几个方面来介绍这个代码。

使用信号与槽函数的基本条件

​ 以下是基本条件:

  1. 拥有信号函数或者槽函数的类,头文件中必须使用Q_OBJECT宏,这个宏会在类中声明一些信号槽机制需要用到的变量以及成员函数,在qmake或者cmake阶段,会根据这个宏,在moc文件中生成对应的调用代码(前提是在qmake或者cmake中添加相关的代码)。
  2. 拥有信号函数或者槽函数的类,必须直接或者间接的继承QObject类。
  3. 程序中需要有QApplication 或者 QCoreApplication 对象,最后有在执行事件循环,否则会有一丢丢的问题,现在先暂时不表。
  4. 信号和槽函数必须使用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() 就可以了。

​ 这个函数一共有五个参数:

  1. 信号发送者
  2. 信号函数
  3. 信号接收者
  4. 槽函数
  5. 绑定类型

​ 第五个参数是有默认值的,可以选择不传。

​ 在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();
}

​ 运行结果:

image-20230616220500137

​ 可以看到,槽函数被触发的线程与主线程一致,这与我一惯的认知是相同的。

​ 然后稍微修改一下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();
}

​ 运行结果:

image-20230616220808087

槽函数不会被触发了?

​ 但是只是将对象创建的位置修改了一下,为什么会导致槽函数不会被触发了呢?

​ 再次修改代码:

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();
}

​ 运行结果:

image-20230616221204022

​ 在子线程中手动调用了事件循环,然后槽函数被触发了,并且它被触发的线程ID是子线程的。

关于跨线程调用的结论
  1. QT文档中指的将槽函数放入列队,并才槽函数对应的线程被事件循环触发。这里的线程指的是对象被创建的线程。
  2. 如果对象创建的线程没有事件循环,也就是不在App对象所在的线程,那么槽函数永远不会被触发,除非在这个线程里使得App对象进入事件循环。

大脸猫

这个人虽然很勤快,但什么也没有留下!

文章评论