Qt 信号槽相关

发布时间 2023-12-18 10:28:51作者: 虚在君

Qt中信号与槽的机制用于实现对象之间的通信,这种机制允许对象在特定事件发生时发送信号,而其他对象可以通过连接到这些信号的槽函数来响应这些事件。

Qt官方的相关文档在此:https://doc.qt.io/qt-5/signalsandslots.html

实现信号槽的功能首先需要两个(可以是同一个)对象,一个负责发送信号一个负责接收信号,然后就是要准备好需要发送的信号,以及接收信号后产生动作的槽函数,而在实际运行前,需要将这些东西连接起来。

信号和槽机制是类型安全的:信号的参数必须与槽函数的参数相匹配。(实际上,槽的参数可以比它接收到的信号参数更少,因为槽可以忽略额外的参数)由于参数是兼容的,所以在使用基于函数指针语法的信号与槽关联机制时,编译器可以帮助检测类型是否匹配,从而可以检测出在开发中信号和槽函数关联时出现的问题。

信号和槽函数是松耦合的:当一个对象发出信号,该对象不知道也不关心哪个对象的槽函数会接收这个信号。Qt的信号和槽函数机制确保:如果将一个信号连接到一个槽函数上,该槽函数将在正确的时间被调用。信号和槽函数可以接受任意数量的任意类型的参数。它们完全是类型安全的。所有从QObject或它的一个子类(例如,QWidget)继承的类都可以使用信号和槽槽函数机制。当对象改变其状态时,可能就会发出信号(这一点由开发人员和父类确定其关联的信号什么时候发出)。

槽函数用来接收信号,但也是普通的成员函数。就像对象不知道是否有东西接收到它的信号一样,槽函数也不知道是否有信号连接到它,因此可以创建独立的软件组件。当需要使用该独立组件时,确定其组件类中预定义的信号和槽函数,然后关联信号和槽函数即可。

可以将多个信号连接到一个槽函数上(即【多对一】),而一个信号也可以连接到多个槽函数上【即一对多】。

也可以将一个信号直接连接到另一个信号。(当第一个信号发出时,它将立即发出第二个信号。)

信号相关

首先,关于发送信号的对象。只有Qt类才能定义信号,所以发送信号的对象必须继承一个Qt类。对于现成的一些控件比如QPushButton,有一些内置的信号可以很方便地进行调用,比如click()。如果要自定义信号函数,首先要遵循以下原则:

1.返回值是void类型

2.函数只能声明不能定义

3.信号必须使用signals关键字进行声明

4.函数的访问属性自动被设置为protected

5.只能通过emit关键字调用函数(发射信号)

 

由于信号是公共访问函数,所以可以从任何地方发出(也就是在任何地方使用emit来调用),但是建议:【只从定义该信号的类及其子类发出信号】。所以一般用法中,在定义该信号的类内,会添加一个send函数专门用来发送信号。

 

当一个信号发出时,连接到它的槽函数通常会立即执行,就像一个普通函数调用一样。在这种情况下,信号和槽函数机制是完全独立于GUI事件循环的,也并不会干扰GUI的事件循环。emit语句之后的代码将在所有槽函数都返回之后才执行。如果使用排队连接(queued connections),情况略有不同,在这种情况下,emit关键字后面的代码将立即继续,槽函数将在后续执行。

如果几个槽函数连接到同一个信号上,当信号发出时,这些槽函数将按照它们连接时的顺序依次执行【这一点很重要】。

信号是由moc工具自动生成,不能在.cpp文件中实现,所以信号永远不能有返回类型(必须使用void关键字定义信号)。

关于信号和槽参数的注意事项:经验表明,如果信号和槽函数不使用特殊类型,那么代码具有极强的可重用性。

 

自定义信号的示例代码如下:

//TestSignal.h
class TestSignal : public QObject   //只有Qt类才能定义信号
{
    Q_OBJECT    //必须使用宏Q_OBJECT
public:
    void send(int i)
    {
        emit testSignal(i);     //通过emit关键字发射信号
    }   
signals:                    //使用signals声明信号函数,访问级别为protected
    void testSignal(int v);     //信号只能声明不能定义
};
//RxClass.h
class RxClass : public QObject
{
    Q_OBJECT
protected slots:
    void mySlot(int v)
    {
        qDebug() << "void mySlot(int v)";
        qDebug() << "Sender: " << sender()->objectName();
        qDebug() << "Receiver: " << this->objectName();
        qDebug() << "Value: " << v;
        qDebug() << endl;
    }
};

//main.cpp
void emit_signal()
{
    qDebug() << "emit_signal()" << endl;

    TestSignal t;
    RxClass r;

    t.setObjectName("t");
    r.setObjectName("r");

    //信号函数与槽函数需要一致,并且不出现参数名
    QObject::connect(&t, SIGNAL(testSignal(int)), &r, SLOT(mySlot(int)));

    for(int i=0; i<3; i++)
    {
        t.send(i);
    }
}

int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);

    emit_signal();
    
    return a.exec();
}

示例中也出现了槽函数的定义和将信号与槽函数连接起来的方法,这些接下来讲述。

槽函数相关

槽函数是普通的C++函数,在实际开发中也可以正常调用;它们唯一的特点是:【信号可以与它们相连接】。

由于槽是普通的成员函数,所以它们在直接调用时遵循普通的C++规则。但是,作为槽函数时,任何组件都可以通过信号连接从而调用它们。

还可以将槽函数定义为虚拟的,这在开发中非常有用。

与回调机制相比,信号和槽函数机制的速度稍微慢一些,这一点对于实际应用程序来说,这种差别并不显著。一般来说,发送一个连接到某些槽函数的信号,比直接调用非虚函数要慢大约10倍。这是定位连接对象、安全地遍历所有连接(即检查后续接收方在发射过程中没有被销毁)以及以函数调用增加的开销。虽然10个非虚函数调用听起来很多,但是它比new操作或delete操作的开销要小得多。一旦在后台执行一个需要new或delete的字符串、向量或列表操作,信号和槽函数的开销只占整个函数调用开销的很小一部分。在槽函数中执行系统调用时也是如此(或间接调用超过十个函数)。因此信号和槽函数机制的简单性和灵活性是值得的,这些开销在实际应用场景下甚至不会注意到。

注意,当与基于Qt的应用程序一起编译时,定义为信号或槽的变量的第三方库可能会导致编译器出现警告和错误。要解决这个问题,使用#undef来定义出错的预处理器符号即可。

声明槽需符合以下规则

声明槽需要使用slots关键字,在其后面有一个冒号“:”,且槽需使用public、private、protected访问控制符之一。

 

发射信号需符合以下规则:

  • 发射信号需要使用emit关键字,注意,在emit后面不需要冒号。
  • emit发射的信号使用的语法与调用普通函数相同,比如有一个信号为void f(int),则发送的语法为:emit f(3);
  • 当信号被发射时,与其相关联的槽函数会被调用(注意:信号和槽需要使用QObject::connect函数进行关联之后,发射信号后才会调用相关联的槽函数)。
  • 注意:因为信号位于类之中,因此发射信号的位置需要位于该类的成员函数中或该类能见到信号的标识符的位置。

 

信号和槽的关系

  • 槽的参数的类型需要与信号参数的类型相对应,
  • 槽的参数不能多余信号的参数,因为若槽的参数更多,则多余的参数不能接收到信号传递过来的值,若在槽中使用了这些多余的无值的参数,就会产生错误。
  • 若信号的参数多余槽的参数,则多余的参数将被忽略。
  • 一个信号可以与多个槽关联,多个信号也可以与同一个槽关联,信号也可以关联到另一个信号上。
  • 若一个信号关联到多个槽时,则发射信号时,槽函数按照关联的顺序依次执行。
  • 若信号连接到另一个信号,则当第一个信号发射时,会立即发射第二个信号。

信号与槽的连接

信号和槽通过
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)*函数关联
其中参数type定义了信号和槽的关联方式。

Qt支持6种连接方式:


1.Qt::AutoConnection(自动方式)
如果接收者生活在发出信号的线程中,Qt::DirectConnection被使用。否则,使用Qt::QueuedConnection。连接类型是在信号发出时确定。【这是Qt创建信号和槽函数时的默认连接方式】


2.Qt::DirectConnection(直接连接)(同步)
当信号发送后,相应的槽函数将立即被调用。emit语句后的代码将在所有槽函数执行完毕后被执行。


3.Qt::QueuedConnection(排队连接)(异步)
当信号发出后,排队到信号队列中,需等到接收对象所属线程的事件循环取得控制权时才取得该信号,调用相应的槽函数。emit语句后的代码将在发出信号后立即被执行,无需等待槽函数执行完毕。多线程环境下可使用。


4.Qt::BlockingQueuedConnection(阻塞连接,信号和槽必须在不同的线程中,否则就产生死锁)
发送信号后发送者所在的线程会处于阻塞状态 ,直到槽函数运行完。多线程同步环境下可使用。


5.Qt::UniqueConnection
与默认工作方式相同,只是不能重复连接相同的信号和槽,因为如果重复连接就会导致一个信号发出,对应槽函数就会执行多次。这个flag可以通过按位或(|)与以上四个结合在一起使用


6.Qt::AutoCompatConnection
是为了连接Qt4与Qt3的信号槽机制兼容方式,工作方式与Qt::AutoConnection一样。

使用connect函数将信号连接到槽函数的三种方法:

1、第一种方法:使用函数指针

connect(sender, &QObject::destroyed, this, &MyObject::objectDestroyed);

QObject::connect()与函数指针一起使用有几个优点。它允许编译器检查信号的参数是否与槽的参数兼容。当然,编译器还可以隐式地转换参数。

2、第二种方法:连接到C++ 11的lambdas

connect(sender, &QObject::destroyed, this, [=](){ this->m_objects.remove(sender); });

在这种情况下,我们在connect()调用中提供这个上下文。上下文对象提供关于应该在哪个线程中执行接收器的信息。

当发送方或上下文被销毁时,lambda将断开连接。注意:当信号发出时,函数内部使用的所有对象依然是激活的。

3、第三种方法:使用QObject::connect()以及信号和槽声明宏。

SIGNAL()SLOT()宏中包含参数(如果参数有默认值)的规则是:传递给SIGNAL()宏的参数不能少于传递给SLOT()宏的参数。

例如以下代码都是合法的:

connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed(Qbject*)));
connect(sender, SIGNAL(destroyed(QObject*)), this, SLOT(objectDestroyed()));
connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed()));

​ 但是这种是非法的:

connect(sender, SIGNAL(destroyed()), this, SLOT(objectDestroyed(QObject*)));

因为槽函数期望的是一个信号不会发送的QObject。此连接将报告运行时错误。

注意,使用这种方法时,在使用QObject::connect()关联信号和槽函数时,编译器不会自动检查信号和槽函数的参数之间是否匹配。

综上:使用第一种方法 创建信号和槽 在开发中较为常用,也较为合适。
另外,在Qt Creator界面里可以不使用connect函数完成默认信号与槽函数的连接,右键点击对应组件,然后选择“转到槽”,再选择需要触发的信号,Qt Creator就会自动生成槽函数的定义以及槽函数实现的框架,只要把实现内容填入即可,这个方法的缺点是不容易检查信号与槽函数的连接,因为没有展现connect这一个过程。

信号和槽函数的一些高级用法

当需要获取信号发送方的信息时,使用Qt提供QObject::sender()函数,该函数返回一个指向发送信号对象的指针。

Lambda表达式是传递自定义参数到槽的一种方便方式:

connect(action, &QAction::triggered, engine,[=]() { engine->processAction(action->text()); });

使用disconnect断开信号/槽连接

disconnect()用于断开对象发送器中的信号与对象接收器中的方法的连接。如果连接成功断开,则返回true;否则返回false。

当对信号/槽关联的两方中的任何一个对象进行销毁时,信号/槽连接将被移除。

disconnect()有三种使用方法,如下示例所示:

1、断开所有与对象相连的信号/槽:

disconnect(myObject, nullptr, nullptr, nullptr);

相当于非静态重载函数:

myObject->disconnect();

2、断开所有与特定信号相连的对象:

disconnect(myObject, SIGNAL(mySignal()), nullptr, nullptr);

相当于非静态重载函数:

myObject->disconnect(SIGNAL(mySignal()));

3、断开特定接收对象的连接:

disconnect(myObject, nullptr, myReceiver, nullptr);

相当于非静态重载函数:

myObject->disconnect(myReceiver);

nullptr可以用作通配符,分别表示“任何信号”、“任何接收对象”或“接收对象中的任何槽”。

如下格式的使用示例:

disconnect(发送对象,信号,接收对象,方法)
  • 发送对象不会是nullptr。
  • 如果信号为nullptr,将断开接收对象和槽函数与所有信号的连接。否则只断开指定的信号。
  • 如果接收对象是nullptr,它断开所有关联该信号的连接。否则,只断开与接收对象的槽函数连接。
  • 如果方法是nullptr,它会断开任何连接到接收对象的连接。如果不是,只有命名为方法的槽函数连接将被断开。如果没有接收对象,方法必须为nullptr。即:
disconnect(发送对象,信号,nullptr,nullptr)

传递参数为自定义参数时

Qt的信号和槽可以传递int、double等c++常用类型变量,也可以传递QVector、QMap等Qt的容器类(当然也可以传递Qt定义的类型)。
传递自定义的结构体:
首先在定义结构体的同时需要使用Q_DECLARE_METATYPE。通过这个宏定义可以将自定义的类型注册到Qt的元类型中,从而被Qt识别。

struct PersonInfo
{
    QString Name;
    int age;
};
Q_DECLARE_METATYPE(PersonInfo)

其次在信号端发射的信号类型应该是QVariant,QVariant是多种类型的联合,QVariant类中有个SetValue(T& value)方法,将自定义 T 类型的数据保存到QVariant对象中,可以理解为是自定义类型被封装成了QVariant的形式,这样,我们自定义的T类型的对象就能够通过所有参数和返回值为QVarian类型传递。

signals:
    void  PersonInfoSIG(QVariant info);

在发射信号之前,将自定义的结构体用QVariant包裹一下,就可以发射了。

 QVariant DataInfo;
    DataInfo.setValue(info);
    emit PersonInfoSIG(DataInfo);

在槽函数这边,信号类型也定义为QVariant,接收后用该结构体将数据取出来就完成了自定义结构体信号的一次传递

 PersonInfo ReInfo=info.value<PersonInfo>();

本文参照了以下博客的内容:

Qt原理分析(三):Qt中自定义信号_qt 自定义signal-CSDN博客

Qt一篇全面的信号和槽函数机制总结 - 知乎 (zhihu.com)

Qt信号与槽使用方法最完整总结 - AI观星台 - 博客园 (cnblogs.com)

Qt信号与槽原理 - 知乎 (zhihu.com)

QT信号与槽的6种连接方式以及传递参数为自定义参数时_qt信号槽传递参数-CSDN博客