本节讲解信号和槽的概念,以及标准的信号槽
首先看一下什么是事件和信号
以QPushButton
的单击事件为例:
mousePressEvent
事件然后QPushButton
会发射pressed()
信号mouseReleaseEvent
事件然后QPushButton
会发射released()
信号和clicked()
信号具体一个类有哪些信号,可以查看Qt的帮助文档,以QPushButton
为例:
首先打开QPushButton
的帮助说明:
接看,跳转到其父类QAbstractButton
,这里就有信号了,如下 :
点击Signals
可以跳转到信号处,如下:
可以点击对应的链接,查看详细说明
这里总结如下:
// 当按钮被点击(按下并抬起)之后,发送该信号,其中带有一个默认参数
// 对于QPushbutton 通常不需要传递这个默认参数
// 对于可选中/取消选中的按钮,比如复选框QCheckBox,单选框QRadioButton 可以通过该参数,获取其是否选中
void clicked(bool checked = false);
// 当按钮被按下时,发送该信号
void pressed();
// 当按钮被抬起时,发送该信号
void released();
// 当按钮状态改变时,发送该信号,其中带有一个参数checked
// checked 用于标识复选框QCheckBox,单选框QRadioButtons是否被选中
void toggled(bool checked);
以后遇到其他类的信号和槽,就按照这个方法来查阅即可。
我们通常说的槽,就是槽函数
当点击了QPushButton
按钮之后,通常需要执行对应的操作,比如让QMainWindow
窗口最大/最小/正常化显示,或者关闭窗口
按照以上查看信号的方法,查看QMainWindow
提供了哪些槽函数,同样跳转到其父类QWidget
中查看,如下:
可以点击对应的链接,查看详细说明,比如:
// 最大化显示
void showMaximized();
// 最小化显示
void showMinimized();
// 关闭窗口
bool close();
讲解了信号和槽之后,如何实现如下效果呢?
效果:点击按钮居,实现窗口的最大/最小正常显示,或者关闭窗口。
答:就需要将信号和槽使用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
接受到信号之后,调用的函数
信号和槽遵循 松散耦合
一句话总结:信号槽是对象之间的信息通讯的方式
这里以一个实际的案例,来演示信号和槽的使用
案例:点击对应按钮,实现窗口的最大化/最小化/正常显示窗口,和关闭窗口
界面布局
HorizontalSpacer
(避免按钮太大,影响美观);连接信号槽
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()));
标准信号槽中,信号和槽函数,都是Qt框架定义好的。
Qt 还允许我们自定义信号和槽。
自定义信号和槽的条件:
QObject
Q_OBJECT
只有满足了这两个条件才可以正常使用信号槽机制。
接下来,我们通过一个案例,演示自定义信号槽的使用。
案例:“长官"(Commander)发送一个"冲"(go)的信号,然后“士兵"(Soldier)执行"战斗"(fight)的槽函数
创建Commander
l类
在signals下面添加自定义的信号即可
// 只需要声明 不需要实现
void go();
创建Soldier
类
public slot
下或者 public
或者全局函数Soldier士兵类,需要实现一个fight的槽函数
在 Soldier.h文件中
// 槽函数的返回值和参数,要和信号保持一致
void fight();
在 Soldier.cpp文件中
void Soldier::fight() {
qDebug() << "fight";
}
信号和槽都已经定义完毕,接下来就可以进行连接了
在mainwindow.cpp
中,定义Commander
和Soldier
类的实例,并建立信号和槽的连接,如下:
// 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");
我们知道,信号和槽的本质就是函数,是函数就可以重载,因此,我们可以重载同名的信号,和重载同名的槽函数。
仍然以Commander
和Soldier
为例:
在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;
}
如果要使用信号和槽,需要满足如下两个条件
QObject
Q_OBJECT
只有满足了这两个条件才可以正常使用信号槽机制。
signals
关键字修饰。void
,参数的类型和个数不限。emit
关键字,emit
也可以省略。slots
关键字修饰(其实在Qt5中可以省略slots关键字)信号和槽要建立连接,本质上是通过connect
函数来连接实现的。
但是从写法或者者操作上来说,有多种方式,以下总结了5种方式:
接下来通过一个案例,来演示这5种使用方法:
布局展示:
通过SIGNAL/SLOT
这两个宏,将函数名以及对应的参数,转换为字符串,这是Qt4中使用的方式,当然在Qt5中也是兼容支持它的:
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
其中:
sender
:信号发送者SIGNAL()
:发送的信号SIGNAL
:宏将信号转换成字符串receiver
:信号接收者SLOT()
:槽函数。 SLOT
宏将槽函数转换成字符串这种方式,编译器不会做错误检查,即使函数名或者参数写错了,也可以编译通过,这样就把问题留在了运行阶段。
而我们编程开发的一个原则是尽可能早地发现并规避问题,因此这种方式不被推荐。
下面通过这种方式,实现点击按钮,最大化窗口
在mainwindow.cpp
的构造函数中,使用如下方式连接信号和槽:
// 1. 使用SIGNAL/SLOT方式连接信号槽
connect(ui->btnMax, SIGNAL(clicked()), this, SLOT(showMaximized()));
如果函数的名字写错了,在编译时不会报错,而在运行时才会发现有错误。
这种方式中,信号和槽都使用函数的地址,如下:
connect(sender, &sender::signal, receiver, &Receiver::slot);
其中:
sender
:信号发送者&Sender:signal
:发送的信号receiver
:信号接收者&Receiver:slot
:槽函数这种方式,编译时就会对函数类型,参数个数做检查。
下面通过这种方式,实现点击按钮,正常化显示窗口
在mainwindow.cpp
的构造函数使用如下方式连接信号和槽:
// 2. 使用函数地址的方式连接信号槽
connect(ui->btnNormal, &QPushButton::clicked, this,
&QMainWindow::showNormal);
如果函数的名字写错了,在编译时就会报错。
可见,使用这种方式,错误在代码编辑)程序编译阶段就会暴露出来,及早解决掉它。
这是Qt推荐的一种连接信号槽的方式!
选中控件按钮 右键弹出点击转到槽
选择clicked()
,即可生成并跳转到槽函数,即可在mainwindow.h
和mainwindow.cpp
中生成对应的代码,如下
// mainwindow.h
private slots:
void on_btnMin_clicked();
// mainwindow.cpp
void MainWindow::on_btnMin_clicked() {
this->showMinimized();
}
此时会根据按钮的name自动生成对应的槽函数,对应关系为:
按钮的名字:btnMIn
槽函数的名字为:on_btnMin_clicked
注意:如果修改了按钮的name,那么槽函数的名字也要随之修改。
下面使用这种方式,实现点击btnClose按钮,关闭窗口
进入到UI设计师界面,我们可以用可视化连接信号和槽,无需写代码。
此时,我们的代码文件并没有修改,而是修改了mainwindow.ui
文件
mainwindow.ui
文件 最终会被转换为ui_mainwindow.h
文件,打开就可以看到,转换后还是通过connect()
来连接的信号和槽
槽函数还可以直接写成Lambda
表达式的形式。
C++11引入了Lambda
表达式,用于定义并创建匿名的函数对象,可以使代码更加的简洁。
C++中的lambda表达式,其实就是匿名函数,语法如下
[capture](parameters) option -> return type { body }
其中包含5个部分:
capture
:捕获列表,可选
捕捉列表总是出现在Lambda
表达式的开始。实际上,[]
是Lambda
引l出符,编译器根据该引出符判断接下来的代码是否是Lambda
表达式。
捕捉列表能够捕获上下文中的变量,以在Lambda
表达式内使用,主要有如下几种情况:
// 不捕获任何变量
[]
// 按引用捕获外部作用域中所有变量, 在Lambda 表达式内使用
[&]
// 按值捕获外部作用域中所有变量,在Lambda表达式内使用
// 按值捕获的变量,在Lambda表达式内是只读的,不能修改赋值
[=]
// 按值捕获 a变量,同时不捕获其他变量
[a]
// 捕获当前前中的this指针
// 捕获了 this 就可以在Lambda 中使用当前类的成员变量和成员函数
// 如果已经使用了 & 或者 = 就默认添加此选项
[this]
parameters
:参数列表,可选
option
:函数选项,可选
return-type
:返回值类型,可选。没有返回值的时候也可以连同符号->
一起省略
body
:函数体
补充:mutable
修饰 值传递变量,可以修改拷贝出的数据,改变不了本体。
我们使用Lambda来自定义槽函数
// 5. 使用Lambda表达式连接槽函数
connect(ui->btnSetWindowTitle, &QPushButton::clicked, this, [=]() {
this->setWindowTitle("通过按钮修改标题");
在信号和槽存在重载时,Qt4和Qt5的写法是有区别的:
SIGNAL/SLOT
中指定函数参数类型,因此写法比较简单。接下来,以上面的 中“长官和士兵”的例子为例,来看下信号槽重载时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");
一个信号可以连接多个槽函数,如下:
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();
可以将多个信号连接到同一个槽函数,如下:
connect(sender, SIGNAL(signal1), receiver, SLOT(fun()));
connect(sender, SIGNAL(signal2), receiver, SLOT(fun()));
这样,当signal1
和singnal2
这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();
信号不仅可以连接槽,还可以和连接信号,如下:
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设计师界面创建一个按钮
然后,实例化commander
和soldier
两个对象,并连接信号槽,如下:
// 信号连接信号
// 首先,成员变量初始化
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
信号的发射,会去执行soldier
的escape
槽函数
注意
commander
和soldier
要定义为类的成员变量。commander
和soldier
定义为局部变量,MainWindow
构造执行完毕后,这两个变量就已经释放了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 **自动取消所有连接到这个对象上面的槽。