Qt 信号和槽

文章目录

  • 1 标准信号和槽
    • 1.1 什么是信号和槽
      • 1.1.1 信号
      • 1.1.2 槽
    • 1.2 标准信号和槽的使用
  • 2 自定义信号和槽
    • 2.1 自定义信号和槽案例
      • 2.1.1 创建Commander类
      • 2.1.2 添加自定义信号
      • 2.1.3 添加Soldier类
      • 2.1.4 添加自定义槽
      • 2.1.5 连接自定义的信号和槽
      • 2.1.6 信号和槽的重载
    • 2.2 信号和槽总结
      • 2.2.1 使用信号和槽的条件
      • 2.2.2 信号
      • 2.2.3 槽
  • 3 信号和槽 - 多种连接方式
    • 3.1 SIGNAL / SLOT (Qt4)
      • 3.1.1 实现窗口最大化
      • 3.1.2 编译不检查
    • 3.2 函数地址 (Qt5)
      • 3.2.1 实现窗口正常显示
      • 3.2.2 编译检查
    • 3.3 UI设计师界面 - 转到槽
    • 3.4 UI设计师界面 - 信号槽编辑器
    • 3.5 Lambda 表达式
      • 3.5.1 Lambda 表达式
      • 3.5.2 槽函数使用Lambda表达式
  • 4 信号和槽 - 扩展
    • 4.1 如何连接重载的信号和槽
    • 4.2 一个信号连接多个槽
    • 4.3 多个信号连接一个槽
    • 4.4 信号连接信号
    • 4.5 断开连接 - disconnect

1 标准信号和槽

本节讲解信号和槽的概念,以及标准的信号槽

1.1 什么是信号和槽

1.1.1 信号

首先看一下什么是事件和信号

QPushButton的单击事件为例:

  • 按下按钮,会触发mousePressEvent事件然后QPushButton会发射pressed()信号
  • 松开按钮,会触发mouseReleaseEvent事件然后QPushButton会发射released()信号和clicked()信号
  • 常用的事件有很多,比如鼠标的单击和双击事件,鼠标的移动事件,键盘的输入事件等。
  • 当某个实例化的对象上产生这些事件时,该实例化对象就会发出特定的信号。
  • 信号的本质就是函数,进目是只需声明,无需实现的函数

具体一个类有哪些信号,可以查看Qt的帮助文档,以QPushButton为例:

首先打开QPushButton的帮助说明:

Qt 信号和槽_第1张图片

接看,跳转到其父类QAbstractButton,这里就有信号了,如下 :

Qt 信号和槽_第2张图片

点击Signals可以跳转到信号处,如下:

Qt 信号和槽_第3张图片

可以点击对应的链接,查看详细说明

这里总结如下:

// 当按钮被点击(按下并抬起)之后,发送该信号,其中带有一个默认参数  
// 对于QPushbutton 通常不需要传递这个默认参数  
// 对于可选中/取消选中的按钮,比如复选框QCheckBox,单选框QRadioButton 可以通过该参数,获取其是否选中
void clicked(bool checked = false);

// 当按钮被按下时,发送该信号
void pressed();

// 当按钮被抬起时,发送该信号
void released();

// 当按钮状态改变时,发送该信号,其中带有一个参数checked
// checked 用于标识复选框QCheckBox,单选框QRadioButtons是否被选中
void toggled(bool checked);

以后遇到其他类的信号和槽,就按照这个方法来查阅即可。

1.1.2 槽

我们通常说的,就是槽函数

当点击了QPushButton按钮之后,通常需要执行对应的操作,比如让QMainWindow窗口最大/最小/正常化显示,或者关闭窗口

按照以上查看信号的方法,查看QMainWindow提供了哪些槽函数,同样跳转到其父类QWidget中查看,如下:

Qt 信号和槽_第4张图片

可以点击对应的链接,查看详细说明,比如:

// 最大化显示   
void showMaximized();

// 最小化显示
void showMinimized();

// 关闭窗口
bool close();

讲解了信号和槽之后,如何实现如下效果呢? Qt 信号和槽_第5张图片

效果:点击按钮居,实现窗口的最大/最小正常显示,或者关闭窗口。

答:就需要将信号和槽使用connect函数进行连接

比如将QPushButton按钮的clicked()信号和QMainWindow窗口的close()槽函数建立连接之后,当点击了QPushButton按钮后,Qt框架就会自动调用QMainWindow窗口的close()槽函数,从而实现窗口的关闭。

connect方法是Qobject类的静态方法,它有多个重载的方法,如下:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Qt4和Qt5中连接信号和槽的方法略有不同,后面会详细说明。

不过总的来说,connect函数的一般形式如下:

connect(sender, signal, receiver, solt);

其中:

  • sender

    发送信号的对象。比如QPushButton 按钮

  • signal

    发出的信号。比如clicked()

  • receiver

    接收信号的对象。比如QMainWindows窗口

  • slot

    接受到信号之后,调用的函数

信号和槽遵循 松散耦合

一句话总结:信号槽是对象之间的信息通讯的方式

1.2 标准信号和槽的使用

这里以一个实际的案例,来演示信号和槽的使用

案例:点击对应按钮,实现窗口的最大化/最小化/正常显示窗口,和关闭窗口

Qt 信号和槽_第6张图片

界面布局

Qt 信号和槽_第7张图片

  • 放置按钮并布局
    • 放置4个按钮;
    • 选中窗口,点击工具栏的【水平布局】,进行布局(这样可以对齐并且自适应窗口大小的变化);
    • 在左右两侧各放一个HorizontalSpacer(避免按钮太大,影响美观);

连接信号槽

Qt 信号和槽_第8张图片

connect(ui->btnMax, SIGNAL(clicked()), this, SLOT(showMaximized()));
connect(ui->btnNormal, SIGNAL(clicked()), this, SLOT(showNormal()));
connect(ui->btnMin, SIGNAL(clicked()), this, SLOT(showMinimized()));
connect(ui->btnClose, SIGNAL(clicked()), this, SLOT(close()));

2 自定义信号和槽

标准信号槽中,信号和槽函数,都是Qt框架定义好的。

Qt 还允许我们自定义信号和槽。

自定义信号和槽的条件:

  • 自定义的类,要继承自QObject
  • 自定义的类,其中要声明一个宏Q_OBJECT

只有满足了这两个条件才可以正常使用信号槽机制。

2.1 自定义信号和槽案例

接下来,我们通过一个案例,演示自定义信号槽的使用。

案例:“长官"(Commander)发送一个"冲"(go)的信号,然后“士兵"(Soldier)执行"战斗"(fight)的槽函数

2.1.1 创建Commander类

创建Commanderl类

Qt 信号和槽_第9张图片

2.1.2 添加自定义信号

  1. 需要声明,不需要实现
  2. 可以有参数,可以重载

在signals下面添加自定义的信号即可

Qt 信号和槽_第10张图片

// 只需要声明 不需要实现
void go();

2.1.3 添加Soldier类

创建Soldier

Qt 信号和槽_第11张图片

2.1.4 添加自定义槽

  1. 需要声明,也需要实现
  2. 槽函数可以:写到 public slot 下或者 public 或者全局函数
  3. 可以有参数,可以重载

Soldier士兵类,需要实现一个fight的槽函数

在 Soldier.h文件中

Qt 信号和槽_第12张图片

// 槽函数的返回值和参数,要和信号保持一致
void fight();

在 Soldier.cpp文件中

Qt 信号和槽_第13张图片

void Soldier::fight() {
    qDebug() << "fight";
}

2.1.5 连接自定义的信号和槽

信号和槽都已经定义完毕,接下来就可以进行连接了

mainwindow.cpp中,定义CommanderSoldier类的实例,并建立信号和槽的连接,如下:

Qt 信号和槽_第14张图片

// 1. 创建两个类实例
    // this传递父类指针加入对象树中, 自动析构对象
    Commander *commander = new Commander(this);
    Soldier *soldier = new Soldier(this);

    // 2. 建立信号和槽的连接
    connect(commander, SIGNAL(go()), soldier, SLOT(fight()));
    connect(commander, SIGNAL(go(QString)), soldier, SLOT(fight(QString)));

    // 3. 发送信号
    // emit 可以省略
    // emit commander->go();
    commander->go();
    commander->go("Freedom");

2.1.6 信号和槽的重载

我们知道,信号和槽的本质就是函数,是函数就可以重载,因此,我们可以重载同名的信号,和重载同名的槽函数。

仍然以CommanderSoldier为例:

commander.h中添加重载的go信号:

void go(QString);

soldier.h中添加重载的fight槽函数

// 在soldier.h中
void fight(QString);

// 在soldier.cpp中
void Soldier::fight(QString str) {
    qDebug() << "fight for " << str;
}

2.2 信号和槽总结

2.2.1 使用信号和槽的条件

如果要使用信号和槽,需要满足如下两个条件

  1. 自定义的类,要继承自QObject
  2. 自定义的类,其中要声明一个宏Q_OBJECT

只有满足了这两个条件才可以正常使用信号槽机制。

2.2.2 信号

  • 信号的本质是函数,并且只需要声明,不需要实现。
  • 信号用signals关键字修饰。
  • 信号返回值类型为void,参数的类型和个数不限。
  • 信号的发射,可以使用emit关键字,emit也可以省略。
  • 信号本质是函数 因此可以重载。

2.2.3 槽

  • 槽的本质是函数,需要实现。
  • 槽函数用slots关键字修饰(其实在Qt5中可以省略slots关键字)
  • 槽函数的返回值和类型要和信号保持一致。因此槽函数的返回值也是void,参数个数要 小于等 信号的参数个数,通常参数个数保持一致即可。
  • 槽本质是函数,因此可以重载。

3 信号和槽 - 多种连接方式

信号和槽要建立连接,本质上是通过connect函数来连接实现的。

但是从写法或者者操作上来说,有多种方式,以下总结了5种方式:

  1. SIGNAL / SLOT (Qt4)
  2. 函数地址 (Qt5)
  3. UI设计师界面 - 转到槽
  4. UI设计师界面 - 信号槽编辑器
  5. Lambda 表达式

接下来通过一个案例,来演示这5种使用方法:

Qt 信号和槽_第15张图片

布局展示:

Qt 信号和槽_第16张图片

3.1 SIGNAL / SLOT (Qt4)

通过SIGNAL/SLOT这两个宏,将函数名以及对应的参数,转换为字符串,这是Qt4中使用的方式,当然在Qt5中也是兼容支持它的:

connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));

其中:

  1. sender:信号发送者
  2. SIGNAL():发送的信号
  3. SIGNAL:宏将信号转换成字符串
  4. receiver:信号接收者
  5. SLOT():槽函数。 SLOT 宏将槽函数转换成字符串

这种方式,编译器不会做错误检查,即使函数名或者参数写错了,也可以编译通过,这样就把问题留在了运行阶段

而我们编程开发的一个原则是尽可能早地发现并规避问题,因此这种方式不被推荐。

3.1.1 实现窗口最大化

下面通过这种方式,实现点击按钮,最大化窗口

mainwindow.cpp的构造函数中,使用如下方式连接信号和槽:

// 1. 使用SIGNAL/SLOT方式连接信号槽
connect(ui->btnMax, SIGNAL(clicked()), this, SLOT(showMaximized()));

3.1.2 编译不检查

如果函数的名字写错了,在编译时不会报错,而在运行时才会发现有错误。

3.2 函数地址 (Qt5)

这种方式中,信号和槽都使用函数的地址,如下:

connect(sender, &sender::signal, receiver, &Receiver::slot);

其中:

  1. sender:信号发送者
  2. &Sender:signal:发送的信号
  3. receiver:信号接收者
  4. &Receiver:slot:槽函数

这种方式,编译时就会对函数类型,参数个数做检查。

3.2.1 实现窗口正常显示

下面通过这种方式,实现点击按钮,正常化显示窗口

mainwindow.cpp的构造函数使用如下方式连接信号和槽:

// 2. 使用函数地址的方式连接信号槽
connect(ui->btnNormal, &QPushButton::clicked, this,
            	&QMainWindow::showNormal);

3.2.2 编译检查

如果函数的名字写错了,在编译时就会报错。

可见,使用这种方式,错误在代码编辑)程序编译阶段就会暴露出来,及早解决掉它。

这是Qt推荐的一种连接信号槽的方式!

3.3 UI设计师界面 - 转到槽

选中控件按钮 右键弹出点击转到槽

Qt 信号和槽_第17张图片

Qt 信号和槽_第18张图片

选择clicked(),即可生成并跳转到槽函数,即可在mainwindow.hmainwindow.cpp中生成对应的代码,如下

// mainwindow.h
private slots:
    void on_btnMin_clicked();

// mainwindow.cpp
void MainWindow::on_btnMin_clicked() {
    this->showMinimized();
}

此时会根据按钮的name自动生成对应的槽函数,对应关系为:

按钮的名字:btnMIn

槽函数的名字为:on_btnMin_clicked

注意:如果修改了按钮的name,那么槽函数的名字也要随之修改。

3.4 UI设计师界面 - 信号槽编辑器

下面使用这种方式,实现点击btnClose按钮,关闭窗口

进入到UI设计师界面,我们可以用可视化连接信号和槽,无需写代码。

Qt 信号和槽_第19张图片

此时,我们的代码文件并没有修改,而是修改了mainwindow.ui文件

mainwindow.ui文件 最终会被转换为ui_mainwindow.h文件,打开就可以看到,转换后还是通过connect()来连接的信号和槽

3.5 Lambda 表达式

槽函数还可以直接写成Lambda表达式的形式。

C++11引入了Lambda表达式,用于定义并创建匿名的函数对象,可以使代码更加的简洁。

3.5.1 Lambda 表达式

C++中的lambda表达式,其实就是匿名函数,语法如下

[capture](parameters) option -> return type { body }

其中包含5个部分:

  1. capture:捕获列表,可选

    捕捉列表总是出现在Lambda表达式的开始。实际上,[]Lambda引l出符,编译器根据该引出符判断接下来的代码是否是Lambda表达式。

    捕捉列表能够捕获上下文中的变量,以在Lambda表达式内使用,主要有如下几种情况:

    // 不捕获任何变量
    []
    
    // 按引用捕获外部作用域中所有变量, 在Lambda 表达式内使用
    [&]
    
    // 按值捕获外部作用域中所有变量,在Lambda表达式内使用
    // 按值捕获的变量,在Lambda表达式内是只读的,不能修改赋值
    [=]
    
    // 按值捕获 a变量,同时不捕获其他变量
    [a] 
    
    // 捕获当前前中的this指针
    // 捕获了 this 就可以在Lambda 中使用当前类的成员变量和成员函数
    // 如果已经使用了 & 或者 = 就默认添加此选项
    [this]
    
  2. parameters:参数列表,可选

  3. option:函数选项,可选

  4. return-type:返回值类型,可选。没有返回值的时候也可以连同符号->一起省略

  5. body:函数体

补充:mutable 修饰 值传递变量,可以修改拷贝出的数据,改变不了本体。

3.5.2 槽函数使用Lambda表达式

我们使用Lambda来自定义槽函数

// 5. 使用Lambda表达式连接槽函数
    connect(ui->btnSetWindowTitle, &QPushButton::clicked, this, [=]() {
        this->setWindowTitle("通过按钮修改标题");

4 信号和槽 - 扩展

4.1 如何连接重载的信号和槽

在信号和槽存在重载时,Qt4和Qt5的写法是有区别的:

  • Qt 4 方式
    • 可以在SIGNAL/SLOT中指定函数参数类型,因此写法比较简单。
  • Qt5 方式
    • 指定信号和槽时,只能指定函数名,无法向Qt4那样指定函数参数类型,需要单独定义函数指针,写法上稍显麻烦。

接下来,以上面的 中“长官和士兵”的例子为例,来看下信号槽重载时Qt4 和Qt5 写法的不同

**Qt 4 的方式 **

    // 1. Qt4信号槽的连接:SIGNAL/SLOT
    Commander *commander = new Commander(this);
    Soldier *soldier = new Soldier(this);

    // 连接信号和槽
    connect(commander, SIGNAL(go()), soldier, SLOT(fight()));
    connect(commander, SIGNAL(go(QString)), soldier, SLOT(fight(QString)));

    // 触发信号
    commander->go();
    commander->go("freedom");

**Qt 5 的方式 **

    // 1. Qt5 信号槽的连接:函数地址
    Commander *commander = new Commander(this);
    Soldier *soldier = new Soldier(this);

    // 没有重载的信号和槽时,可以直接这样写。因为不存在二义性
    // connect(&commander,&commander::go,&soldier,&soldier::fight);

    // 有重载的信号和槽时,需要向下面这样定义函数指针。因为存在二义性
    // 编译器自动推断:将无参的信号go和无参的槽,赋值给函数指针(ctrl+鼠标点击可以智能跳转过去)
    void (Commander::*pGo)() = &Commander::go;
    void (Soldier::*pFight)() = &Soldier::fight;

    // 连接信号和槽
    connect(commander, pGo, soldier, pFight);

    void (Commander::*pGo1)(QString) = &Commander::go;
    void (Soldier::*pFight1)(QString) = &Soldier::fight;
    // 编译器自动推断:将有参的信号go和有参的槽,赋值给函数指针(ctr1+鼠标点击可以智能跳转过去)
    connect(commander, pGo1, soldier, pFight1);

    // 触发信号
    commander->go();
    commander->go("freedom");

4.2 一个信号连接多个槽

一个信号可以连接多个槽函数,如下:

connect(sender, SIGNAL(signal), receiver1, SLOT(fun1()));
connect(sender, SIGNAL(signal), receiver2, SLOT(fun2()));

这样,当signal1这个信号发出时,它连接的2个槽函数fun1, fun2都会被执行,并且 :

  • Qt 4

    • 信号发射时,与之相连接的槽函数的执行顺序是随机的。
  • Qt 5以上

    • 信号发射时,这些槽函数的执行顺序与建立连接的顺序相同。

    接下来,以“长官和士兵”的例子为例:

    // 士兵1很勇敢,收到冲锋的信号后,开始战斗
        connect(commander, SIGNAL(go()), soldier1, SLOT(fight()));
        
    // 兵2很怕死、收到冲锋的信号后.开始逃跑
    connect(commander, SIGNAL(go()), soldier2, SLOT(escape()));
    

    接下来一步步实现这个需求:

    // 在Soldier.h 中
    void escape();
    
    // 在Soldier.cpp 中
    void Soldier::escape() {
        qDebug() << "escape";
    }
    

然后,连接信号槽并发送信号,如下:

// 一个信号连接多个槽
    Commander *commander = new Commander(this);
    Soldier *soldier1 = new Soldier(this);
    Soldier *soldier2 = new Soldier(this);

    // 士兵1很勇敢,收到冲锋的信号后,开始战斗
    connect(commander, SIGNAL(go()), soldier1, SLOT(fight()));

    // 兵2很怕死、收到冲锋的信号后.开始逃跑
    connect(commander, SIGNAL(go()), soldier2, SLOT(escape()));

    // 触发信号
    commander->go();

4.3 多个信号连接一个槽

可以将多个信号连接到同一个槽函数,如下:

connect(sender, SIGNAL(signal1), receiver, SLOT(fun()));
connect(sender, SIGNAL(signal2), receiver, SLOT(fun()));

这样,当signal1singnal2这2个信号发出时,都会执行槽函数fun()

接下来,以中“长官和士兵"的例子为例:

// 当commander发射go信号和move信号时,都会执行士兵的fight槽函数,开始战斗
    connect(commander, SIGNAL(go()), soldier, SLOT(fight()));
    connect(commander, SIGNAL(move()), soldier, SLOT(fight()));

接下来一步步实现这个需求:

首先,在Commander类中新添加一个move的信号,如下:

// Commander.h
void move();

然后,连接信号槽并发送信号,如下:

    // 多个信号连接一个槽函数
    Commander *commander = new Commander(this);
    Soldier *soldier = new Soldier();

    // 当commander发射go信号和move信号时,都会执行士兵的fight槽函数,开始战斗
    connect(commander, SIGNAL(go()), soldier, SLOT(fight()));
    connect(commander, SIGNAL(move()), soldier, SLOT(fight()));

    // 触发信号
    commander->go();
    commander->move();

4.4 信号连接信号

信号不仅可以连接槽,还可以和连接信号,如下:

connect(obj1, SIGNAL(signal1), obj2, SIGNAL(signal2));

这样,当obj1发送signal1信号时,就会触发obj2发送signal2信号。

接下来,同样以中“长官和士兵"的例子为例:

// 当commander发射go信号和move信号时,都会执行士兵的fight槽函数,开始战斗  

接下来一步步实现这个需求:

首先,在MainWindow.h中新添加两个成员变量,如下:

//在Mainwindow中,添加commander和soldier两个指针类型的成员变量
    Commander *commander;
    Soldier *soldier;

在UI设计师界面创建一个按钮

Qt 信号和槽_第20张图片

然后,实例化commandersoldier两个对象,并连接信号槽,如下:

// 信号连接信号
    // 首先,成员变量初始化
    commander = new Commander();
    soldier = new Soldier();

    // 然后,信号连接信号 + 信号连接槽
    // 信号连接信号
    connect(ui->btnAction, &QPushButton::clicked, commander, &Commander::move);
    // 信号连接槽
    connect(commander, &Commander::move, soldier, &Soldier::escape);

此时,点击按钮,按钮会发射clicked信号,接着commander发射move信号,move信号的发射,会去执行soldierescape槽函数

注意

  • 此时的commandersoldier要定义为类的成员变量。
  • 因为如果把commandersoldier定义为局部变量,MainWindow构造执行完毕后,这两个变量就已经释放了

4.5 断开连接 - disconnect

disconnect用于断开信号和槽之间已经建立的连接

这种情况并不常用,因为当一个对象delete之后,Qt 自动取消所有连接到这个对象上面的槽。

// 信号连接信号
    // 首先,成员变量初始化
    commander = new Commander();
    soldier = new Soldier();

    // 然后,信号连接信号 + 信号连接槽
    // 信号连接信号
    connect(ui->btnAction, &QPushButton::clicked, commander, &Commander::move);
    // 断开所有连接到commander信号上的槽函数
    ui->btnAction->disconnect();
    // 信号连接槽
    connect(commander, &Commander::move, soldier, &Soldier::escape);

当然了,disconnect有多个重载的函数,具体参考Qt 帮助文档即可。

pe);


此时,点击按钮,按钮会发射`clicked`信号,接着`commander`发射`move`信号,`move`信号的发射,会去执行`soldier`的`escape`槽函数  



**注意**  

- 此时的`commander`和`soldier`要定义为类的成员变量。  
- 因为如果把`commander`和`soldier`定义为局部变量,`MainWindow`构造执行完毕后,这两个变量就已经释放了



## 4.5 断开连接 - disconnect

`disconnect`用于断开信号和槽之间已经建立的连接 

这种情况并不常用,因为当一个对象`delete`之后,**Qt** 自动取消所有连接到这个对象上面的槽。 

```c++
// 信号连接信号
    // 首先,成员变量初始化
    commander = new Commander();
    soldier = new Soldier();

    // 然后,信号连接信号 + 信号连接槽
    // 信号连接信号
    connect(ui->btnAction, &QPushButton::clicked, commander, &Commander::move);
    // 断开所有连接到commander信号上的槽函数
    ui->btnAction->disconnect();
    // 信号连接槽
    connect(commander, &Commander::move, soldier, &Soldier::escape);

当然了,disconnect有多个重载的函数,具体参考Qt 帮助文档即可。

disconnect函数并不常用,因为当一个对象delete之后,**Qt **自动取消所有连接到这个对象上面的槽。

你可能感兴趣的:(#,信号和槽,Qt,qt,开发语言,c++)