Featured image of post QT学习记录_04

QT学习记录_04

Qt Study Record Day4

Qt信号与槽机制

信号槽是 Qt 框架引以为豪的机制之一。所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,将想要处理的信号和自己的一个函数(称为槽(slot))绑定来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。

使用系统自带的信号和槽实现关闭窗口功能

首先需要创建一个按钮用于触发“关闭窗口”信号:

1
QPushButton *btn = new QPushButton("关闭窗口", this);

然后我们可以通过connect函数进行信号的连接操作,connect函数的一般形式为:

1
connect(sender, signal, receiver, slot);

参数解释:

  1. sender:发出信号的对象
  2. signal:发送对象发出的信号(函数的地址)
  3. receiver:接收信号的对象
  4. slot:接收对象在接收到信号之后所需要调用的函数(槽函数)

此处即体现出信号和槽机制松散耦合的优点:信号发送端和接收端本身没有关联,但可以通过connect进行连接将两端耦合在一起。 随后,填入相应实参即可调用connect函数进行信号连接。

1
connect(btn, &QPushButton::clicked, this, &Widget::close);

那么系统自带的信号和槽通常如何查找呢?这个就需要利用帮助文档了,比如这里我们需要的是QPushButton的点击信号,在帮助文档中输入QPushButton,首先我们可以在Contents中寻找关键字 signals,但发现并没有找到,这时候我们应该想到也许这个信号的被父类继承下来的,因此我们去他的父类QAbstractButton中就可以找到该关键字,点击signals索引到系统自带的信号有如下几个

1
2
3
4
5
6
void clicked(bool checked = false)
void pressed()
void released()
void toggled(bool checked)
  - 3 signals inherited from QWidget
  - 2 signals inherited from QObiect

这里的clicked就是我们需要的signal函数,槽函数的寻找方式和信号一样,只不过他的关键字是slot。

自定义信号与槽函数

定义两个继承自QObject的类——Teacher和Student,实现一个场景:上课铃响后,老师喊上课,学生们收到上课的信号回教室坐好。

  • teacher.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#ifndef TEACHER_H
#define TEACHER_H

#include <QObject>

class Teacher : public QObject
{
    Q_OBJECT
public:
    explicit Teacher(QObject *parent = nullptr);

signals:
    // 自定义信号 写到signal下
    // 返回值是void,只需要声明,不需要实现
    // 可以有参数,可以发生重载
    void ClassBegin();

public slots:
    // 自定义槽函数,写到slot下
};

#endif // TEACHER_H
  • student.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef STUDENT_H
#define STUDENT_H

#include <QObject>

class Student : public QObject
{
    Q_OBJECT
public:
    explicit Student(QObject *parent = nullptr);

signals:

public slots:
    // 早期Qt版本必须要写到public slots下,高级版本可以写到public或者全局下
    // 返回值void,需要声明也需要实现
    // 可以有参数,也可以发生重载
    void SitDown();
};

#endif // STUDENT_H
  • student.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include "student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent)
{

}
// 自定义成员函数记得加函数返回值类型
void Student::SitDown(){
    qDebug() << "学生都坐好" << endl;
}
  • widget.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "teacher.h"
#include "student.h"

QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

private:
    Ui::Widget *ui;
    // 声明teacher和student类型成员变量
    Teacher *tp;
    Student *sp;
    // 声明上课铃响函数
    void Ring();
};
#endif // WIDGET_H
  • widget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "widget.h"
#include "ui_widget.h"
#include <QPushButton>

Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{
    ui->setupUi(this);
    QPushButton *btn = new QPushButton("关闭窗口", this);
    /*
     * 新建信号连接函数
     * 参数1:信号发送者
     * 参数2:发送的具体的信号(函数的地址)
     * 参数3:信号的接收者
     * 参数4:信号的处理——槽函数地址(槽)
     * 信号和槽的优点——松散耦合:信号发送端和接收端本身没有关联,但可以通过connect进行连接将两端耦合在一起
    */
    // 参数2和参数4写父类子类都可以
    connect(btn, &QPushButton::clicked, this, &Widget::close);
    // 创建对象并指定父窗口
    this->tp = new Teacher(this);
    this->sp = new Student(this);
    // 情景:上课铃响后,老师发出上课信号,学生响应信号,坐好
    // 老师说上课,学生坐好的连接
    connect(tp, &Teacher::ClassBegin, sp, &Student::SitDown);
    // 调用上课铃响函数
    Ring();
}
// 自定义的类方法记得加返回值类型
void Widget::Ring(){
    // 上课函数,调用后触发老师喊上课的信号
    emit tp->ClassBegin();
}

Widget::~Widget()
{
    delete ui;
}

此时的代码执行流程为:
调用Ring()函数–>触发老师喊上课信号–>回调connect函数–>学生收到信号–>调用SitDown函数

重载信号与槽函数

信号和槽函数均可发生重载

  • student.h
1
void SitDown(QString ClassName);
  • student.cpp
1
2
3
4
5
6
7
8
void Student::SitDown(QString ClassName){
    // 直接使用此方式打印会使得ClassName带引号(QString类型字符串),需要转换为char*类型字符串
    // qDebug() << "学生都坐好并拿出" << ClassName << "课本" << endl;
    // QString -> char* 先转换成QByteArray再转成char*
    // 下面这种方式会多出一个空格
    // qDebug() << "学生都坐好并拿出" << ClassName.toUtf8().data() << "课本" << endl;
    qDebug() << ("学生都坐好并拿出" + ClassName + "课本").toUtf8().data() << endl;
}
  • teacher.h
1
void ClassBegin(QString ClassName);
  • widget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{
    ui->setupUi(this);
    ...
    // 创建对象并指定父窗口
    this->tp = new Teacher(this);
    this->sp = new Student(this);
    // 情景:上课铃响后,老师发出上课信号,学生响应信号,坐好
    // 老师说上课,学生坐好的连接
    // 当发生信号或槽函数重载后,connect无法分清是哪个函数发生了重载,需要通过函数指针重新指定函数地址
    // connect(tp, &Teacher::ClassBegin, sp, &Student::SitDown);
    // 函数指针写法:
    // 函数返回值类型(命名空间:: *指针名)(参数类型1, 参数类型2...) = 函数地址
    void(Teacher:: *teachersignal)(QString) = &Teacher::ClassBegin;
    void(Student:: *studentslot)(QString) = &Student::SitDown;
    connect(tp, teachersignal, sp, studentslot);
    // 调用上课铃响函数
    Ring();
}
// 自定义的类方法记得加返回值类型
void Widget::Ring(){
    // 上课函数,调用后触发老师喊上课的信号
    // emit tp->ClassBegin();
    emit tp->ClassBegin("语文");
}

使用信号连接信号

程序情景:点击一个上课的按钮,再触发上课铃响,然后老师叫同学们坐好上课。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 通过函数指针重新指定有参函数的地址
void(Teacher:: *teachersignal)(QString) = &Teacher::ClassBegin;
void(Student:: *studentslot)(QString) = &Student::SitDown;
// 通过函数指针重新指定无参函数的地址
void(Teacher:: *tsignal)(void) = &Teacher::ClassBegin;
void(Student:: *ssignal)(void) = &Student::SitDown;
connect(tp, teachersignal, sp, studentslot);
// 使用按钮调用Ring函数
QPushButton *classbtn = new QPushButton("上课", this);
classbtn->move(100, 0);
connect(classbtn, &QPushButton::clicked, this, &Widget::Ring);

程序情景:点击上课的按钮直接触发老师喊上课信号,然后老师喊同学们坐好上课。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void(Teacher:: *teachersignal)(QString) = &Teacher::ClassBegin;
void(Student:: *studentslot)(QString) = &Student::SitDown;
// 通过函数指针重新指定无参函数的地址
void(Teacher:: *tsignal)(void) = &Teacher::ClassBegin;
void(Student:: *ssignal)(void) = &Student::SitDown;
connect(tp, teachersignal, sp, studentslot);
// 使用按钮调用Ring函数
QPushButton *classbtn = new QPushButton("上课", this);
classbtn->move(100, 0);
// connect(classbtn, &QPushButton::clicked, this, &Widget::Ring);
// 使用按钮直接连接老师上课信号(有参),让老师上课信号作为槽函数再连接学生坐好的槽函数
connect(classbtn, &QPushButton::clicked, tp, tsignal);
connect(tp, tsignal, sp, ssignal);

程序情景:点击上课按钮触发老师喊上课的信号,但学生不听老师的,不坐好

1
2
// 使用disconnect函数断开连接
disconnect(tp, tsignal, sp, ssignal);

Qt4版本信号和槽的写法

1
connect(zt,SIGNAL(ClassBegin(QString)),st,SLOT(SitDown(QString)));

这里使用了SIGNALSLOT这两个宏,将两个函数名转换成了字符串。注意到**connect()**函数的 signalslot 都是接受字符串,一旦出现连接不成功的情况,Qt4是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。 Qt5在语法上完全兼容Qt4,而反之是不可以的。

Lambda表达式

Lambda是属于C++11的新特性。在早期版本(Qt4之前)需要在.pro文件中添加:CONFIG += c++11
C++中的Lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。Lambda表达式的基本构成为:

1
2
3
4
// 函数定义:
[函数对象参数](操作符重载函数参数)mutable->返回值类型{函数体}
// 函数调用:
();

函数对象参数[],标识一个Lambda的开始,必须存在。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义Lambda为止时Lambda所在作用范围内可见的局部变量(包括Lambda所在类的this)。函数对象参数有以下形式:

函数对象参数 解析
没有使用任何函数对象参数。
= 函数体内可以使用Lambda所在作用范围内所有可见的局部变量(包括Lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
this 函数体内可以使用Lambda所在类中的成员变量。若在connect函数中使用this时,前面的信号接收者(或信号发送者)可省略
a 将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
&a 将a按引用进行传递。
a, &b 将a按值进行传递,b按引用进行传递。
=, &a, &b 除a和b按引用进行传递外,其他参数都按值进行传递。
&, a, b 除a和b按值进行传递外,其他参数都按引用进行传递。

操作符重载函数参数标识重载的()操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。
可修改标示符即mutable声明,这部分可以省略。按值传递函数对象参数时,加上mutable修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身,再次访问传入的函数对象参数时仍是原值)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
QPushButton * myBtn = new QPushButton (this);
    QPushButton * myBtn2 = new QPushButton (this);
    myBtn2->move(100,100);
    int m = 10;
    // 点击按钮输出110
    connect(myBtn,&QPushButton::clicked,this,[m] ()mutable { m = 100 + 10; qDebug() << m; });
    // 点击按钮输出10
    connect(myBtn2,&QPushButton::clicked,this,[=] ()  { qDebug() << m; });
    // 输出10
    qDebug() << m;

函数返回值->返回值类型,标识函数返回值的类型。当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。
函数体{}标识函数的实现,这部分不能省略,但函数体可以为空。

使用Lambda表达式的优点:

在connect函数中使用Lambda表达式,可以更加方便地使用有参槽函数。

信号和槽总结

  • 信号可以连接信号
  • 一个信号可以连接多个槽函数。如果是这种情况,这些槽会一个接一个的被调用,但是它们的调用顺序是不确定的。
  • 多个信号也可以连接同一个槽函数。只要任意一个信号发出,这个槽就会被调用。
  • 信号和槽函数的参数类型必须一一对应
  • 信号的参数个数可以多于槽函数的参数个数,但除了多出来的参数,剩下的参数类型仍然要一一对应。
  • 信号槽可以通过disconnect函数断开连接
  • 槽可以被取消链接。但这种情况并不经常出现,因为当一个对象delete之后,Qt自动取消所有连接到这个对象上面的槽。
  • 发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
  • 信号和槽函数返回值是 void
  • 信号只需要声明,不需要实现
  • 槽函数需要声明也需要实现
  • 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
  • 使用 emit 在恰当的位置发送信号;
  • 使用connect()函数连接信号和槽。
  • 任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数

Picture

Licensed under CC BY-NC-SA 4.0
热爱可抵岁月漫长,温柔可挡艰难时光。
Nothing but enthusiasm brightens up the endless years.
转载请注明主页网址哦~