一、信号和槽概述
信号槽是Qt矿建引以为豪的机制之一。
所谓信号槽,实际上就是观察者模式(发布——订阅模式)。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号。这种发出的信号是没有目的的,类似于广播。如果有对象对这个信号感兴趣,它就会使用连接函数(connect),意思是将想要处理的信号和自己的一个函数(称为槽)绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自定被回调。
这就类似于观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。
1.1 信号的本质
信号是由于用户对窗口或控件进行了某些操作,导致窗口或控件产生了某个特定事件,这时候Qt对应的窗口类会发出某个信号,对用以此户的挑选做出反应。
根据上面的论述,我们可以得出一个结论——信号的本质就是事件,例如:
- 按钮单击、双击
- 窗口刷新
- 鼠标移动、鼠标按下、鼠标释放
- 键盘输入
在Qt中,信号是通过什么形式呈现给使用者的呢??
- 我们对哪个窗口进行操作,哪个窗口就可以捕捉到这些被触发的事件
- 对于使用者来说,触发一个事件,我们就可以得到Qt框架给我们发出的某个特定信号
- 信号的呈现形式就是函数,也就是说某个事件产生了,Qt框架就会调用某个对应的信号函数,通知使用者。
在Qt中信号的发出者是某个实例化的类对象,对象内部可以进行相关事件的检测。
1.2 槽的本质
在Qt中,槽函数是一类特殊功能的函数,在编码过程中,也可以作为类的普通成员函数来使用。之所以称之为槽函数,是因为它们还有一个职责就是对Qt框架中产生的信号进行处理。
举个例子:
有一天,我们在和女朋友一起逛街,突然,女朋友说:“我肚子饿了!”,于是我们带着她们去吃饭。(这就相当于女朋友发出一个信号,我收到信号并将其处理掉)
实例对象 | 角色 | 描述 |
女朋友 | 信号发出者 | 信号携带的信息:我饿了 |
我 | 信号接收者 | 处理女朋友发射的信号:带她去吃饭 |
在Qt中槽函数的所有者也是某个类的实例对象。
1.3 信号和槽的关系
在Qt中,信号和槽函数都是独立的个体,本身没有任何联系,但是由于某种特性需求,我们可以将二者连接在一起,好比牛郎和织女想要见面,必须要有喜鹊为他们搭桥。
在Qt中,我们需要使用QObject类中的connect函数进行两者的关联。
连接信号和槽的connect()函数原型如下,其中PointerToMenberFunction是一个指向函数地址的指针:
QMetaObject::Connection QObject::connect( const QObject* sender, PointerToMemberFunction signal, const QObject* receiver, PointerToMenberFunction method, Qt::ConnectionType type = Qt::AutoConnection); // 参数: // sender:发出信号的对象 // signal:属于sender对象,信号是一个函数,这个参数的类型是函数指针,信号函数地址 // receiver:信号接受者 // method:属于receiver对象,当检测到sender发出了signal信号,receiver对象调用method方法,信号发出之后的处理动作 // 参数signal和method都是函数地址,因此简化后的connect()如下: connect(const QObject* sender, &QObject::signal, const QObject* receiver, &QObject::method);
使用connect()函数进行信号槽的连接的注意事项:
- connect函数相对于做了信号处理动作的注册
- 调用connect函数的sender对象的信号没有产生,因此receiver对象的method也不会被调用
- method槽函数本质是一个回调函数,调用的时机是信号产生之后,调用的是Qt框架来执行的
- connect连接中的sender和receiver两个指针必须被实例化,否则connect不会成功
二、标准信号槽的使用
2.1 标准信号/槽
在Qt中提供了很多标准类中都可以对用户触发的某种特定事件进行检测,因此当用户做了这些操作之后,事件被触发类的内部就会产生对应的信号,这些信号都是Qt类内部自带的,因此称之为标准信号。
同样地,在Qt的很多类的内部中为我提供了很多功能函数,并且这些函数也可以作为触发信号的处理动作,有这类特性的函数在Qt中称之为标准槽函数。
系统自带的信号和槽通常如何查找?这个就需要利用帮助文档,比如在帮助文档中查询按钮的点击信号,那么需要在帮助文档中输入QPushButton
2.2 使用
举个例子:
功能实现:点击窗口上的按钮,关闭窗口
功能分析:按钮:信号发出者 QPushButton 类型
窗口:信号的接收者和处理处 QWidget 类型
// 需要使用的标准信号槽函数 // 单击按钮发出的信号 void QAbstractButton::clicked(bool check = false); // 关闭窗口的槽函数 bool QWidget::close(); // 点击按钮关闭窗口 connect(ui->closewindow, &QPushButton::clicked, this, &MainWindow::close);
connect()操作一般写在窗口的构造函数汇总,相当于在事件产生之前,在Qt框架中先进行注册,这样在程序运行过程中假设产生了按钮的点击事件,框架就会调用信号接收者对象对应的槽函数,如果信号不产生,槽函数也就一直不会被调用。
三、自定义信号槽的使用
Qt框架提供的信号槽在某种特定的场景下是无法满足我们的项目的要求,因此我们还设计自己需要的信号和槽,同样还是使用connect()对自定义的信号槽进行连接。
如果想要在Qt类中自定义信号槽,需要满足一些条件,并且也要注意一些事情:
- 要编写新的类并且让其继承Qt的某些标准类
- 这个新的子类必须从QObject类或者是QObject子类进行派生
- 在定义类的头文件中假如Q_OBJECT宏
// 在头文件派生类的时候,首先像下面那样引入Q_OBJECT宏 class MyMainWindow : public QWidget { Q_OBJECT ...... }
3.1 自定义信号
在Qt中信号的本质是事件,但是在框架中也是以函数的形式存在的,只不过信号对应的函数只有声明,没有定义。如果Qt中的标准信号不能满足我们的需求,可以在程序中进行信号的自定义,当自定义信号对应的时间产生之后,认为将这个信号发射出去即可(其实就是调用一下这个信号函数)
自定义信号的要求和注意事项:
- 信号是类的成员函数
- 返回值必须是void类型
- 信号的名字可以根据实际情况进行指定
- 参数可以随意指定,信号也支持重载
- 信号需要使用signals关键字进行声明,使用方法类似于public等关键字
- 信号函数只需要声明,不需要定义(没有函数实现)
- 在程序中发射自定义信号:发送信号的本质就是调用信号函数。习惯性在信号函数前加关键字:emit,但是可以省略不写,emit只是显示的声明一下信号要被发射了,没有特殊含义,底层中 emit == #define emit
class Test : public Qobject { Q_OBJECT signals: void testsignals(); // 参数的作用是数据传递,谁调用信号函数谁就指定实参 // 实参最终会被传递给槽函数 void testsignals(int a); }'
3.2 自定义槽
槽函数就是信号的处理动作,在Qt中槽函数可以作为普通的成员函数来使用。如果标准槽函数提供的功能满足不了需求,可以自己定义槽函数进行某些特殊功能的实现。自定义槽函数和自定义的普通函数写法是一样的。
自定义槽的要求和注意事项:
- 返回值必须是void类型
- 槽也是函数,因此也支持重载
- 槽函数需要指定多少个参数,需要看连接信号的参数个数(连接信号的参数个数大于槽函数的参数个数)
- 槽函数的参数是用来接收信号传递的数据,信号传递的数据就是信号的参数
- Qt中的槽函数类型是多样的,Qt中的槽函数可以是类的成员函数,全局函数,静态函数,Lambda表达式(匿名函数)
- 槽函数可以使用关键字进行声明:slots(Qt5中slots可以省略不写)
再来通过一个例子来自定义一个信号槽:还是上面的场景:女朋友说:“我肚子饿了!”,于是我带她去吃饭
class GirlFriend : public Object { Q_OBJECT public: explicit GirlFriend(QObject* parent = nullptr); signals: void hungry(); void hungry(QString msg); }; class Me : public Object { Q_OBJECT public: explicit Me(QObject* parent = nullptr); public slots: // 槽函数 void eatMeal(); void eatMeal(QString msg); };
四、信号槽的扩展
4.1 信号槽使用扩展
一个信号可以连接多个槽函数,发送一个信号有多个处理动作:
- 需要写多个connect()连接
- 槽函数的执行顺序是随机的,和connect函数的调用顺序没有关系
- 信号的接收者可以是一个对象,也可以是多个对象
一个槽函数可以连接多个信号,多个不同的信号,处理动作是相同的
- 需要写多个connect()连接
信号可以连接信号
- 信号接收者可以不处理接收的信号,而是继续发射新的信号,这就相当于传递了数据,并没有随数据进行处理
connect(const QObject* sender, &QObject::singal, const QObject* receiver, &QObject::signal_new);
信号槽是可以通过函数断开的
disconnect(const QObject* sender, &QObject::signal, const QObject* receiver, &QObject::method);
4.2 信号槽的连接方式
4.2.1 Qt5的连接方式
// 语法 QMetaObject::Connection QObject::connect( const QObject* sender, PointerToMenmberFunction signal, const QObject* receiver, PointerToMemberFunction method, Qt::ConnectionType = Qt::AutoConnection); // 信号和槽函数也就是第2,4个参数传递的是地址,编译器在编译过程中会对数据的正确性进行检查 connect(const QObject* sender, &QObject::signal, const QObject* receiver, &QObject::method);
4.2.2 Qt4的连接方式
这种旧的信号槽连接方式在Qt5中是支持的,但是不推荐使用,因为这种方式在进行信号槽的连接的时候,信号槽函数通过宏SIGNAL和SLOT转换为字符串类型。
因为信号槽函数的 转换是通过宏来进行转换的,因此传递到宏函数内部的数据不会被进行检测,如果使用者传错了数据,编译器不会报错,但实际上信号槽的连接已经不对了,只有在程序运行起来之后才能发现问题,而且问题不容易被定位。
4.2.3 Qt5处理方式错误的原因
上边的写法之所以错误是因为这个类中信号槽都是重载过的, 信号和槽都是通过函数名去关联函数的地址, 但是这个同名函数对应两块不同的地址, 一个带参, 一个不带参, 因此编译器就不知道去关联哪块地址了, 所以如果我们在这种时候通过以上方式进行信号槽连接, 编译器就会报错。
4.2.4 总结:
- Qt4的信号槽连接方式因为使用了宏函数, 宏函数对用户传递的信号槽不会做错误检测, 容易出bug
- Qt5的信号槽连接方式, 传递的是信号槽函数的地址, 编译器会做错误检测, 减少了bug的产生
- 当信号槽函数被重载之后, Qt4的信号槽连接方式不受影响
- 当信号槽函数被重载之后, Qt5中需要给被重载的信号或者槽定义函数指针
五、Lambda表达式
5.1 语法格式
Lambda表达式就是一个匿名函数,语法格式如下:
[capture](params) opt-> ret{ body; }; --capture:捕获列表 --params:参数列表 --opt:函数选项 --ret:返回值类型 --body:函数体
5.1.1 捕获列表:捕获一定范围内的变量
- [ ]:不捕捉任何变量
- [&]:捕获外部作用域中的所有变量,并作为引用在函数体内使用(按引用捕获)
- [=]:捕获外部作用域中的所有变量 ,并作为副本在函数体内使用(按值捕获),拷贝的副本在匿名函数体内是只读的
- [= , &foo]:按值捕获外部作用域中的所有变量,并按照引用捕获外部变量foo
- [bar]:按值捕获bar变量,同时不捕获其他变量
- [&bar]:按引用捕获bar变量,同时不捕获其他变量
- [this]:捕获当前类中的this指针,让lambda表达式拥有和当前成员函数同样的访问权限,如果已经使用了&或者=,默认添加次选项
5.1.2 参数列表:和普通函数的参数列表一样
5.1.3 opt选项 -> 可以省略
- mutable:可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)
- execption:指定函数抛出的异常,如抛出整数类型的异常,可以使用throw()
5.1.4 返回值类型
表示函数返回值的类型,当返回值为void,或者函数体中只有溢出return的地方(此时编译器可以自动推断出返回值的类型)时,这部分可以省略。
5.1.5 函数体
函数的实现,这部分不能省略,但是函数体可以为空。
5.2 定义和调用
因为Lambda表达式是一个匿名函数,因此是没有函数声明的,直接在程序中进行代码的定义即可,但是如果只定义匿名函数在程序执行过程中是不糊被调用的。
// 匿名函数的定义,程序执行这个匿名函数是不会被调用的 []() { qDebug() << "Hello, I am lambda..."; }; // 匿名函数的定义 + 调用 int ret = [](int a) -> int { return a + 1; }(100); // 100是传递给匿名函数的参数
在Lambda表达式中的捕获列表中也就是[ ] 中添加不同的关键字,就可以在函数体中使用外部变量了。
// 在匿名函数外部定义变量 int a = 100, b = 200, c = 300; // 调用匿名函数 // 使用引用的方式传递数据 [&](){ qDebug() << "a + 1:" << a++ << ", b + c = " << b + c; }(); // 值拷贝的方式使用外部数据 [=](int m, int n) mutable{ qDebug() << "Hello, 我是一个lambda表达式..."; qDebug() << "使用拷贝的方式传递数据:"; // 拷贝的外部数据在函数体内部是只读的,如果不添加mutable关键字是不能修改这些只读数据的值 // 添加mutable允许修改的数据是拷贝到函数内部的副本,对外部数据没有影响 qDebug() << "a + 1:" << a++ << ", b + c = " << b + c; qDebug() << "m + 1:" << ++m << ", n: " << n; }(1, 2);