关于Qt信号槽中connect与disconnect介绍
首先我们要知道,如果想要使用Qt中的信号槽机制, 那么必须继承QObject类,因为QObject类中包含了信号槽的一系列操作,今天我们来讲解的是信号与槽怎么建立连接以及断开连接。
一、connect
QMetaObject::Connection QObject::connect(param p1, param p2, …);
Qt中关于信号槽的connect给出了五种方式,不同connect方法的主要区别是参数的个数及参数类型不同,用来满足不同的场景,但相同的是他们的返回值是Connection类型,关于Connection在上一篇文章中也已经描述过,主要用于表示当前信号与当前槽函数连接的句柄,可以通过这个句柄来判断当前连接是否成功、也可以通过此句柄来定位到当时的连接,以此来断开对应信号与槽的连接。
下面分别来讲解这几种不同方式的用法。
1.1 Qt4中通常写法
connect(const QObject *sender, const char * signal, const QObject *receiver, const char * method , Qt::ConnectionType type = Qt::AutoConnection)
// 示例(输入框文字变动实时显示到Label上);
QLabel *label = new QLabel;
QLineEdit *lineEdit = new QLineEdit;
QObject::connect(lineEdit, SIGNAL(textChanged(const QString&)),
label, SLOT(setText(const QString&)));
用法:
此方式是Qt4中通用使用的方式,参数分别为信号发送的对象,信号函数,信号接收对象,信号触发的槽函数,这种写法就必须要求给==信号和槽函数前加上宏SIGNAL()和SLOT()==来指定,且信号和槽函数后面必须且只需要加上参数类型(如果没有参数,为空就好,如果有参数,切记只需要填每个参数的类型,不需要具体的变量名称,否则连接失败),因为我们看到此方式信号和槽函数对应的参数类型为 const char*,你可以随意填任意的字符串,但我们必须要按照这种方式,Qt内部会根据宏展开具体的操作,所以这种方式Qt不会帮你检查是否填写错误,尽管你随便填也可以编译成功,只不过连接不上罢了,因为参数都是字符串,编译期是不检查字符串是否匹配,所以这种方式你必须自己检查宏SIGNAL()和SLOT()里面的内容是否正确。
SIGNAL()和SLOT()宏,它们会将信号/槽函数及其参数转化为const char *类型。
使用场景:
此方式用于Qt4的项目中,Qt5为了兼容Qt4保留了此方式,此外因为很多落后且还在更新的项目仍在采用Qt4版本的库,同时由于团队合作的方式,有些人习惯于Qt4的写法,有些人则偏向于Qt5,所以项目中正常这两种方式都会存在。
这种方式其实我们在上篇文章中与Qt5的正常connect方式做了对比,使用过程中需要注意SIGNAL()和SLOT()里面的内容是否正确,而且就算出错编译器也不会报错,所以会经常出现信号槽连接失败的问题。
1.2 Qt4中不常用法
connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection)
等同于
connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
区别在于这里的信号和槽函数为 const QMetaMethod & 类型,QMetaMethod 是Qt提供的一个专门的类型,可以用于获取一个对象成员函数的元数据,可以获取此成员函数的类型(普通成员函数/信号/槽函数/构造函数)、签名(类似"setValue(double)"),参数类型,参数名称,返回类型,访问权限等等。
正常情况很少用到,大家可能也没有怎么接触过这种方法,这里大家可以认为等同于上面的方式,也就是使用QMetaMethod对信号/槽函数进行指定。
这种方式其实也是为了兼容Qt4中的写法,日常大家可以跳过这种方式。
下面给大家举个例子:
QMetaMethod signalMethod = QMetaMethod::fromSignal(&MyWgtA::signalA);
int methodIndex = m_wgtB->metaObject()->indexOfMethod("onSlotB()");
// 防止找不到;
if (methodIndex != -1)
{
QMetaMethod slotMethod = m_wgtB->metaObject()->method(methodIndex);
connect(m_wgtA, signalMethod, m_wgtB, slotMethod);
}
// delete;
QMetaMethod参考
https://blog.csdn.net/qq_21291397/article/details/104774421
1.3 Qt4中信号接收对象省略法
connect(const QObject *sender, const char *signal, const char *method, Qt::ConnectionType type = Qt::AutoConnection) const
等同于
connect(sender, signal, this, method, type)
此种方法在调用过程中省略了信号接收的对象指针,默认为this(调用对象本身),也就是在哪个类里面调用connect,就是连接这个类的信号/槽函数。
如下方代码中,MyWgtA类的signalA(QString)信号与MyTestWgt类的槽函数onSlotTest()进行连接。
void MyTestWgt::initConnections()
{
connect(m_wgtA, SIGNAL(signalA(QString)), SLOT(onSlotTest()));
}
1.4 Qt5通常用法
connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection)
// 示例(输入框文字变动实时显示到Label上);
QLabel *label = new QLabel;
QLineEdit *lineEdit = new QLineEdit;
QObject::connect(lineEdit, &QLineEdit::textChanged,
label, &QLabel::setText);
参数中的信号必须在头文件中使用signals显示声明,槽函数可以是任何一个成员函数(如果是外部触发必须是public权限),不需要进行slots显示声明(Qt4中必须得有),所以Qt5这种方式真的方便了很多,上一篇文章中,在给大家分析信号槽连接失败原因的时候,就提到Qt4与Qt5的区别,相信大家看完那篇文章也深有体会。
使用这种方式,connect中可以省略信号和槽的参数,也不需要 SIGNAL()和SLOT() 宏的加持,同时编译的时候也会帮你检查信号和槽函数的参数是否正确(保证信号的参数个数>=槽函数参数的个数,且前置参数类型保持一致),防止程序运行的时候才发现信号槽没连上,这个时候你可能写了一大段代码(包含多个connect连接),还不知道是哪个connect会失败,所以建议使用Qt5的信号槽连接方式。
注意:
使用Qt5这种方式,大家有一点是需要注意,也区别于Qt4,当我们的信号重载的时候,需要配合QOverlord进行使用。
![在这里插入图片描述](https://img-blog.csdnimg.cn/cc894a5fe84846ebb8d4a431aa9f330f.png
我们看到QButtonGroup类的buttonClicked信号是有两个重载的,且参数类型不一样,所以我们在使用的时候需要借助QOverload来指定你是用哪一个信号,可以看到下方的代码,这里的代码是信号发生了重载,如果是槽函数发生了重载,同样需要使用QOverload来指定,确保唯一性。
// 如果参数类型是int;
void MyWidget::onBtnClicked(int btnId)
{}
connect(buttonGroup, QOverload<int>::of(&QButtonGroup::buttonClicked), myWidget, &MyWidget::onBtnClicked);
// 如果参数类型是QAbstractButton*;
void MyWidget::onBtnClicked(QAbstractButton* pBtn)
{}
connect(buttonGroup, QOverload<QAbstractButton *>::of(&QButtonGroup::buttonClicked), myWidget, &MyWidget::onBtnClicked);
1.5 Qt5信号直连法
connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
这种方式较为简单,我称之为信号直连法,这里不需要接收信号的对象,我们看到这里只有三个参数,且第三个参数是一个仿函数,相当于信号发出后直接调用仿函数。
A function can be connected to a given signal if the signal has at least as many argument as the slot. A functor can be connected to a signal if they have exactly the same number of arguments.
The connection will automatically disconnect if the sender is destroyed. However, you should take care that any objects used within the functor are still alive when the signal is emitted.
这里提供了两种方式:
a:仿函数
void someFunction()
{
// todo;
}
QPushButton *button = new QPushButton;
QObject::connect(button, &QPushButton::clicked, someFunction);
b:Lambda表达式
QPushButton *button = new QPushButton;
QObject::connect(button, &QPushButton::clicked, [=](){
// todo;
});
1.6 Qt5信号直连法2
connect(const QObject *sender, PointerToMemberFunction signal, const QObject *context, Functor functor, Qt::ConnectionType type = Qt::AutoConnection)
此种用法基本和上面的类似,区别在于此方法有了Qt::ConnectionType参数,可以决定当前槽函数的执行方式。
这里也提供了两种方式:
a:仿函数
void someFunction()
{
// todo;
}
QPushButton *button = new QPushButton;
QObject::connect(button, &QPushButton::clicked, this, someFunction);
b:Lambda表达式
QPushButton *button = new QPushButton;
QObject::connect(button, &QPushButton::clicked, this, [=](){
// todo;
});
二、disconnect
上面讲解了信号槽的6种连接方式,现在讲讲断开连接的方法 disconnect。
一般我们用的比较多的是信号槽的连接,断开可能用的相对少,有的初学者可能还不知道有disconnect这个方法,以为信号槽连接就完事了,就比如一个按钮点击就是需要触发一个方法来完成一系列的操作,如果你把他断开连接了,那这个按钮不是没用了吗,那不一定哦。正常情况下,我们的信号会和一个槽函数绑定,但是Qt允许一个信号可以绑定多个槽函数,当我们想断开其中一个连接的时候就需要使用disconnect去断开这一个,其他几个连接可以保持正常,当然也可以全部断开。
举个例子哈,比如我们给好友发消息,信号是发送按钮点击这个信号(signalSendBtnClicked),这个信号我们需要做两个操作(也就是一个信号对应两个槽函数),一个是将发送的文字显示到我们的聊天窗口,一个是将内容发送给服务器进行转发,正常情况下是做这样两个操作。
情况一:如果对方断线,那我们只需要把内容显示到我们的界面上,但是不需要发送到服务器,所以这里就需要断开一个槽函数。
情况二:如果我们自己断线了,我们实际上什么也做不了,因为你已经离线了,所以点击发送就无需做任何操作,我们就断开所有对应的槽函数即可。
disconnect 方法相对connect简单一些,操作起来也非常方便灵活,Qt提供了几种方式供我们使用.
需要注意的是connect方法都是静态方法,直接调用即可;而disconnect分为静态方法和成员方法两种调用方式(静态方法我会在前方加上 [static])。
disconnect 方法的返回值是一个bool类型,我们可以根据这个值来判断当前是否成功断开信号槽的连接。
1.1 对应Qt4的通用写法(静态方法)
[static] bool QObject::disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
a、断开对象A的信号signalA()与对象B的槽函数onSlotB()的连接(正常情况下)
connect怎么写的,disconnect就怎么写,即把之前的connect直接换成disconnect。
// 信号槽连接;
connect(lineEdit, SIGNAL(textChanged(const QString&)),
label, SLOT(setText(const QString&)));
// 信号槽断开;
bool isSuccess = disconnect(lineEdit, SIGNAL(textChanged(const QString&)),
label, SLOT(setText(const QString&)));
// 根据返回值判断断开是否成功;
if(isSuccess)
{
qDebug() << "disconnect Success";
}
b、断开对象A所有信号的连接:
只需要填写对象A,其他都默认填写nullptr即可断开此对象所有信号的相关连接。
disconnect(myObjectA, nullptr, nullptr, nullptr);
等价于:
// 成员方法(对应下方1.5);
myObjectA->disconnect();
c、断开对象A的信号signalA()所有的连接:
disconnect(myObjectA, SIGNAL(signalA()), nullptr, nullptr);
等价于:
// 成员方法(对应下方1.5);
myObjectA>disconnect(SIGNAL(signalA()));
d、断开对象A与对象B所有的信号槽连接:
disconnect(myObjectA, nullptr, myReceiverB, nullptr);
等价于:
// 成员方法(对应下方1.6);
myObjectA->disconnect(myReceiverB);
关于此方法中的参数,我们可以通过空指针nullptr来充当通配符,但是sender参数必须非空,因为disconnect只能断开某一个对象相关信号的连接,具体就是上述四种用法。
1.2 对应Qt4的不常用法-QMetaMethod(静态方法)
[static] bool QObject::disconnect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method)
此方式就是通过对应信号和槽函数的QMetaMethod来断开对应的连接,这里就不举例说明了,不推荐这种方法。
1.3 connect返回句柄断开法(静态方法)
[static] bool QObject::disconnect(const QMetaObject::Connection &connection)
这种方式需要记录之前信号槽连接的句柄,然后通过此句柄来断开唯一对应的信号与唯一对应的槽函数的连接,也不是很推荐。
m_pBtn = new QToolButton;
// 得到句柄;
QMetaObject::Connection cc = connect(m_pBtn, SIGNAL(clicked()), this, SLOT(onBtnCLicked()));
// 根据connect返回的句柄断开相应的信号槽连接;
disconnect(cc);
1.4 对应Qt5的通用写法(静态方法)
[static] bool QObject::disconnect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method)
QObject::disconnect(lineEdit, &QLineEdit::textChanged,
label, &QLabel::setText);
这个方式跟1.1基本一致,也是四种写法,可以使用空指针nullptr来充当通配符,就是注意将参数 const char*换成PointerToMemberFunction 类型即可。
即:
SIGNAL(textChanged(const QString&) 换成 &QLineEdit::textChanged
SLOT(setText(const QString&) 换成 &QLabel::setText)
1.5 断开某个对象的信号连接(成员方法)
bool QObject::disconnect(const char *signal = nullptr, const QObject *receiver = nullptr, const char *method = nullptr) const
与1.1类似,也有四种写法。
// a、断开对象A所有信号的连接:
myObjectA->disconnect();
// b、断开对象A的信号signalA()所有的连接:
myObjectA->disconnect(SIGNAL(mySignal()));
// c、对象A与对象B断开所有信号连接:
myObjectA->disconnect(nullptr, myReceiverB, nullptr);
// d、对象A与对象B断开槽函数onSlotB()所有相关信号的连接::
myObjectA->disconnect(nullptr, myReceiverB, SLOT(onSlotB()));
1.6 对象A与对象B断开相关信号的连接(成员方法)
bool QObject::disconnect(const QObject *receiver, const char *method = nullptr) const
与1.5方法有有类似的效果。
// a、对象A与对象B断开所有信号连接:
myObjectA->disconnect(myReceiverB)
// b、对象A与对象B断开槽函数onSlotB()所有相关信号的连接::
myObjectA->disconnect(myReceiverB, SLOT(onSlotB()));
1.7 注意
如果之前信号连接的是仿函数或者Lambda表达式,我们只能使用1.3里面提到的 QMetaObject::Connection 这种方式来断开连接,因为其他方式没有办法进行唯一匹配。
三、信号槽连接注意点
1、一个信号可以连接一个信号;
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::signalB);
2、一个信号可以同时连接多个信号/槽函数;
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::signalB);
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
connect(myObjectA, &ObjectA::signalA, myReceiverC, &ObjectC::onSlotC);
connect(myObjectA, &ObjectA::signalA, myReceiverD, &ObjectD::onSlotD);
3、多个信号可以连接一个槽函数;
connect(myObjectA, &ObjectA::signalA, myReceiverD, &ObjectD::onSlotD);
connect(myObjectB, &ObjectB:signalB, myReceiverD, &ObjectD::onSlotD);
connect(myObjectC, &ObjectC::signalC, myReceiverD, &ObjectD::onSlotD);
4、如果一个信号连接多个信号/槽函数,那么绑定的信号/槽函数被触发的顺序与建立连接的顺序一致;
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
connect(myObjectA, &ObjectA::signalA, myReceiverC, &ObjectC::onSlotC);
connect(myObjectA, &ObjectA::signalA, myReceiverD, &ObjectD::onSlotD);
emit myObjectA->signalA();
// 发送信号signalA,槽函数执行顺序onSlotB() -> onSlotC() -> onSlotD();
5、正常情况下,如果一个连接被调用两次(多次),则该信号对应的信号/槽函数会被触发两次(多次);
void ObjectB::onSlotB()
{
qDebug() << "Run slotB";
}
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
emit myObjectA->signalA();
// 调用三次输出三次;
Run slotB
Run slotB
Run slotB
6、特殊情况下,如果connect方法中的参数Qt::ConnectionType type = Qt::UniqueConnection,那么表示此次的连接是唯一的,建立过连接之后,再次调用同样的代码建立连接将以失败而告终,也就是只能connect成功一次,后面的connect将失效。
以下代码connect调用了四次,但是失败两次,因为connect中有三次参数 ConnectionType类型为Qt::UniqueConnection,所以只成功了一次,故信号触发,槽函数会被执行两次。
void ObjectB::onSlotB()
{
qDebug() << "Run slotB";
}
// isSucces = true; (第一次调用,所以返回true)
isSucces = connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB, Qt::UniqueConnection);
// isSucces = false; (第二次调用,且ConnectionType 仍然为 Qt::UniqueConnection)
isSucces = connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB, Qt::UniqueConnection);
// isSucces = false; (第三次调用,且ConnectionType 仍然为 Qt::UniqueConnection)
isSucces = connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB, Qt::UniqueConnection);
// isSucces = true;(第四次调用,但是ConnectionType 默认为 AutoConnection)
isSucces = connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
emit myObjectA->signalA();
// 调用四次输出两次;
Run slotB
Run slotB
需要注意的是Qt::UniqueConnection对lambdas表达式或者非成员函数/仿函数不生效。
7、不管对象A的信号signalA()和对象的槽函数onSlotB()的信号槽连接被调用了一次或者多次(信号触发会一次或多次调用槽函数),只要调用了对应的disconnect方法,那么不管调用的多少次的connect方法,所有的连接都会断开。
void ObjectB::onSlotB()
{
qDebug() << "Run slotB";
}
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
disconnect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
emit myObjectA->signalA();
// 调用三次无输出,因为调用了disconnect,所有的连接都断开;
8、信号槽中信号发送方对象sender或者接收方对象receiver其中一个被销毁connect将会自动断开
void ObjectB::onSlotB()
{
qDebug() << "Run slotB";
}
myObjectA = new ObjectA;
myReceiverB = new ObjectB;
connect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
disconnect(myObjectA, &ObjectA::signalA, myReceiverB, &ObjectB::onSlotB);
delete myReceiverB;
emit myObjectA->signalA();
// 无输出,因为对象myReceiverB被销毁;
9、there is an implicit conversion between the types of the corresponding arguments in the signal and the slot
信号和槽中相应参数的类型之间存在隐式转换
class MyWgtA : public QWidget
{
Q_OBJECT
public:
MyWgtA(QWidget *parent = nullptr) {};
signals:
void signalA(double);
};
class MyWgtB : public QWidget
{
Q_OBJECT
public:
MyWgtB(QWidget *parent = nullptr) {};
// 没有明确声明槽函数;
public:
void onSlotB(int param)
{
qDebug() << param;
}
};
void test()
{
MyWgtA* wgtA = new MyWgtA;
MyWgtB* wgtB = new MyWgtB;
connect(wgtA, &MyWgtA::signalA, wgtB, &MyWgtB::onSlotB);
emit wgtA->signalA(12.8);
// 输出12;
}
10、关于信号和槽函数自动绑定的问题,对应的槽函数必须写成on_控件名_信号名的格式,这种情况下Qt提供的元对象系统会提供一个机制自动关联相关的信号和槽函数。
下方是Qt源码中给出的解释;
Qt’s meta-object system provides a mechanism to automatically connect
signals and slots between QObject subclasses and their children. As long
as objects are defined with suitable object names, and slots follow a
simple naming convention, this connection can be performed at run-time
by the QMetaObject::connectSlotsByName() function.
如果出现这种情况,我们仍然再次使用connect去绑定这样的信号与槽函数,会输出以下警告:
QMetaObject::connectSlotsByName: No matching signal for slotName
// 待验证;
这里其实跟setupUi有关系,即带ui文件的界面类,因为designer中我们可以去绑定一个信号和槽函数,我们可以去看下ui文件生成的代码。