一、信号和槽机制概述
信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。
槽的本质是类的成员函数,其参数可以是任意类型的。和普通C++成员函数几乎没有区别,它可以是虚函数;也可以被重载;可以是公有的、保护的、私有的、也可以被其他C++成员函数调用。唯一区别的是:槽可以与信号连接在一起,每当和槽连接的信号被发射的时候,就会调用这个槽。
早期,对象间的通信采用回调来实现。回调实际上是利用函数指针来实现,当我们希望某件事发生时处理函数能够获得通知,就需要将回调函数的指针传递给处理函数,这样处理函数就会在合适的时候调用回调函数。回调有两个明显的缺点:
- 它们不是类型安全的,我们无法保证处理函数传递给回调函数的参数都是正确的。
- 回调函数和处理函数紧密耦合,源于处理函数必须知道哪一个函数被回调。
二、典型应用示例
下面通过一个简单的例子来进一步讲解信号和槽的相关知识。新建Qt Gui应用,项目名称为mySignalSlot,基类选择QWidget,然后类名保持Widget不变。完成后首先在widget.h文件中声明信号和槽:
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QDebug>namespace Ui {
class Widget;
}class Widget : public QWidget
{Q_OBJECT //必须包含的宏public:explicit Widget(QWidget *parent = nullptr);~Widget();public slots:void testSolts(); //测试槽函数signals:void testSignals(); //测试信号private:Ui::Widget *ui;
};#endif // WIDGET_H
使用信号和槽还必须在类声明 的最开始处添加Q_OBJECT宏,在这个程序中,类的声明是自动生成的,已经添加了这个宏。
signal的代码会由 moc 自动生成,开发人员不能在自己的C++代码中实现它。反之,槽应该由开发人员来实现。修改widget.cpp文件如下:
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent) :QWidget(parent),ui(new Ui::Widget)
{ui->setupUi(this);//连接信号槽connect(this,SIGNAL(testSignals()),this,SLOT(testSolts()));//发射信号emit testSignals();
}Widget::~Widget()
{delete ui;
}//测试槽函数
void Widget::testSolts()
{qDebug() << "调用了测试槽函数";
}
这个例子实现的效果是:发射信号后,会调用testSolts()槽函数,则“应用程序输出”窗口会打印出“调用了测试槽函数”。
三、使用信号和槽的注意事项
下面列举一下使用信号和槽应该注意的注意事项:
- 发送者和接收者都需要继承自QObject或其子类;
- 必须在类声明的最开始添加Q_OBJECT宏;
- 槽中的参数类型要和信号的参数的类型相对应,且不能比信号的参数多;
- 使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;
- 使用 emit 在恰当的位置发送信号;
- 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
- 任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数。
四、信号和槽的关联
信号和槽进行关联使用的是QObject类的connect()函数,Qt4中这个函数的原型如下:
static QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal,const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);
第一个参数sender为发送信号的对象,第二个参数signal为要发送的信号,第三个参数receiver为接收信号的对象,第4个参数slot为接收对象在接收到信号之后所需要调用的槽函数。connect()函数的最后一个参数表明了关联的方式,默认值是Qt::AutoConnection。对于信号和槽,必须使用SIGNAL()和SLOT()宏,它们可以将其参数转化为const char*类型。connect()函数的返回值为bool类型,当关联成功时返回true
对于信号和槽的参数问题,基本原则是信号中的参数类型要和槽中的参数类型相对应,而且信号中的参数可以多于槽中的参数,但是不能反过来;如果信号中有多余的参数,那么它们将被忽略。
而Qt5中connect()函数新加入的一种重载形式如下:
static QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal,const QObject *receiver, PointerToMemberFunction member, Qt::ConnectionType type = Qt::AutoConnection);
与Qt4最大的不同就是,指定信号和槽两个参数时可以不用再使用SIGNAL()和SLOT()宏,并且槽函数不再必须是使用slots关键字声明的函数,而可以是任意能和信号关联的成员函数。要使一个成员函数可以和信号关联,那么这个函数的参数数目不能超过信号的参数数目,但是并不要求该函数拥有的参数类型与信号中对应的参数类型完全一致,只需要可以进行隐式转换即可。使用这种重载形式,前面程序中的关联可以使用以下代码代替:
connect(this,&Widget::testSignals,this,&Widget::testSolts);
使用这种方式与前一种相比,还有一个好处就是可以在编译时进行检查,信号与槽的拼写错误、参函数参数数目多于信号的参数数目等错误在编译时就能够发现。所以建议在编写Qt5代码时使用这种关联形式。
信号和槽还有一种自动关联方式,例如在设计模式直接生成的按钮的单击信号的槽,就是使用的这种方式: on_pushButton_clicked() 由字符串on、部件的objectName和信号名称这三部分组成,中间用下划线隔开。这样形式命名的槽可以直接和信号关联,不用再使用connect()函数,不过使用这种方式还要进行其他设置。
五、断开信号和槽的关联
可以通过disconnect()函数来断开信号和槽的关联,其原型如下:
static bool QObject::disconnect(const QObject *sender, const char *signal,const QObject *receiver, const char *member);
(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);
(4)断开一个指定信号和槽的关联
disconnect(myObject, SIGNAL(mySignal()), myReceiver, SLOT(mySlot()));
其用法类似,只是其信号、槽参数需要使用函数指针 &MyObject::mySignal()、&MyReceiver::mySlot() 等形式。这个函数并不能断开信号与一般函数或者lambda表达式之间的关联,如果有这方面需要,则可以使用connect()返回值进行断开。实际上当对象被delete时,其关联的所有链接都会失效,QT会自动移除和这个对象的所有链接。
六、信号和槽的更多用法
(1)一个信号可以连接多个槽
使用QObject::connect可以把一个信号连接到多个槽,而当信号发射时,将按声明联系时的顺序依次调用槽
MyStr a;
MyStr b;
MyStr c;
//信号连接到两个槽
QObject::connect(&a,SIGNAL(valueChanged(QString)),&b,SLOT(setValue(QString)));
QObject::connect(&a,SIGNAL(valueChanged(QString)),&c,SLOT(setValue(QString)));
a.setValue("this is A");
//依次调用b.setValue()、c.setValue()
(2)多个信号可以连接同一个槽
同样的,可以让多个信号连接到同一个槽上 ,而且其中的每一个信号的发送,都会调用了那个槽。
MyStr a;
MyStr b;
MyStr c;
//两个信号连接到同一个槽
QObject::connect(&a,SIGNAL(valueChanged(QString)),&c,SLOT(setValue(QString)));
QObject::connect(&b,SIGNAL(valueChanged(QString)),&c,SLOT(setValue(QString)));
//下面的操作皆会调用到槽c.setValue()
a.setValue("this is A");
b.setValue("this is B");
(3)一个信号可以和另外一个信号相连接
当发射第一个信号的时候,也会把第二个信号一个发送出去。
MyStr a;
MyStr b;
MyStr c;
//两个信号相连接
QObject::connect(&a,SIGNAL(valueChanged(QString)),&b,SIGNAL(valueChanged(QString)));
//再建立b与c的连接
QObject::connect(&b,SIGNAL(valueChanged(QString)),&c,SLOT(setValue(QString)));
//下面的操作同时发送了信号a.valueChanged与b.valueChanged
a.setValue("this is A");
//从而信号b.valueChanged被槽c.setValue所接收