在学习 Qt 的过程中,信号与槽是必不可少的部分,也是 Qt 编程的基础,是 Qt 编程的一大创新(其实与 C#的事件很相似,编程都是类似的),Qt 的信号与槽在Qt4 时或者更早前已经出现,并不是属于哪个版本的。只是 Qt4 与 Qt5 的信号槽连接的写法有些区别。本教程对 Qt4 的信号与槽连接写法不做讲解。
同时,由此章节开始,我们将不使用 Qt Designer 的方式进行开发,也可以在新建项目时把*ui 文件不勾选。为什么不用 Qt Designer 方式开发程序了呢?Qt Designer 方式开发程序简单,优点就是方便快捷,ui 文件的优点就是能比较直观快捷的看到整体的布局。但是缺点也很明显!简单的一两个部件还是可以的,但是控件多了就不好管理,每次都要打开 ui 文件添加新控件,不好用代码管理!读者也无从知道编者是如进行界面布局的,且信号槽的连接方式是生成代码之后才能在 setupUi 函数里才能看到,或者需要进入 Ui 设计器里的信号槽模式里才能看到信号槽的连接,这样十分之不方便!没有代码那么直观!所以统一用代码绘界面,可以锻炼我们的布局能力,和代码逻辑能力!
一、Qt信号与槽机制
信号与槽(Signal & Slot)是 Qt 编程的基础,也是 Qt 的一大创新。因为有了信号与槽的编程机制,在 Qt 中处理界面各个组件的交互操作时变得更加直观和简单。
信号(Signal)就是在特定情况下被发射的事件,例如 PushButton 最常见的信号就是鼠标单击时发射的 clicked() 信号,一个 ComboBox 最常见的信号是选择的列表项变化时发射的CurrentIndexChanged() 信号。
GUI 程序设计的主要内容就是对界面上各组件的信号的响应,只需要知道什么情况下发射哪些信号,合理地去响应和处理这些信号就可以了。
槽(Slot)就是对信号响应的函数。槽就是一个函数,与一般的 C++函数是一样的,可以定义在类的任何部分(public、private 或 protected),可以具有任何参数,也可以被直接调用。槽函数与一般的函数不同的是:槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行。
信号与槽关联是用 QObject::connect() 函数实现的,其基本格式是:
QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
connect() 是 QObject 类的一个静态函数,而 QObject 是所有 Qt 类的基类,在实际调用时可以忽略前面的限定符,所以可以直接写为:
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
其中,sender 是发射信号的对象的名称,
signal() 是信号名称。信号可以看做是特殊的函数,需要带括号,有参数时还需要指明参数。receiver 是接收信号的对象名称,
slot() 是槽函数的名称,需要带括号,有参数时还需要指明参数。
关于信号与槽的使用,有以下一些规则需要注意:
一个信号可以连接多个槽,例如:
connect(pushButton, SIGNAL(clicked()), this, SLOT(hide());
connect(pushButton, SIGNAL(clicked()), this, SLOT(close());
这是当一个对象 pushButton 的被单击时,所在窗体有两个槽进行响应,一个 hide()用于隐藏主窗体,一个 close 用于关闭主窗体。这里只是举个例子,实际上当执行 close()后,hide()这个槽已经没有什么意义了,因为已经程序退出了,但是实际它有执行,因为它连接在 close()的前面。当一个信号与多个槽函数关联时,槽函数按照建立连接时的顺序依次执行。
当信号和槽函数带有参数时,在 connect()函数里,要写明参数的类型,但可以不写参数名称。
多个信号可以连接同一个槽,
一个信号可以连接另外一个信号(说明了 connect 万物皆可连,非常好用!),例如:
connect(pushButton, SIGNAL(objectNameChanged(QString)),this, SIGNAL(windowTitelChanged(QString)));
这样,当一个信号发射时,也会发射另外一个信号,实现某些特殊的功能。严格的情况下,信号与槽的参数个数和类型需要一致,至少信号的参数不能少于槽的参数。如果不匹配,会出现编译错误或运行错误。
在使用信号与槽的类中,必须在类的定义中加入宏 Q_OBJECT(特别重要)。
一个信号被发射时,与其关联的槽函数通常被立即执行,就像正常调用一个函数一样。只有当信号关联的所有槽函数执行完毕后,才会执行发射信号处后面的代码。
总结如下图,可以看到发送者与发送的信号是在一起的,接收者与接收的信号/槽是在一起的。它们不能在 connect()方法里写乱顺序!由发送者发送出信号到接收者用信号/槽接收。
信号槽连接的方法已经讲解了。这里只是稍稍提一下,断开连接的方法,初学者基本用不到断开连接的操作。使用 disconnect()。disconnect(),这个方法重载了好几个函数,格式如下。
bool QObject::disconnect(const QObject *sender, const char *signal, const QObject *receiver, const char *method)
断开一切与 myObject 连接的信号或槽。
disconnect(myObject, 0, 0, 0);
相当于非静态重载函数:
myObject->disconnect();
断开所有连接到特定信号的东西。
disconnect(myObject, SIGNAL(mySignal()), 0, 0);
相当于非静态重载函数:
myObject->disconnect(SIGNAL(mySignal()));
与指定的接收者断开连接。
disconnect(myObject, 0, myReceiver, 0);
相当于非静态重载函数:
myObject->disconnect(myReceiver);
信号与槽机制是 Qt GUI 编程的基础,使用信号与槽机制可以比较容易地将信号与响应代码关联起来。
二、如何在项目里创建信号
我们先新建一个项目,并把项目命名为 03_signal_slot_example,同时取消勾选*ui 文件,其他步骤不变。
由于信号只需声明,无需定义。所以我们只需要在 mianwindow.h 里声明信号即可。代码如下,如下图黑色加粗部分代码就是创建的信号。这里创建一个 void pushButtonTextChanged(); 信号,定义信号最好是贴合信号本身的含义。编者定义这个信号的意思是 按钮的文本发生改变后的 signal。
1 #ifndef MAINWINDOW_H
2 #define MAINWINDOW_H
3
4 #include <QMainWindow>
5 /* 引入 QPushButton */
6 #include <QPushButton>
7
8 class MainWindow : public QMainWindow
9 {
10 Q_OBJECT
11
12 public:
13 MainWindow(QWidget *parent = nullptr);
14 ~MainWindow();
15
16 signals:
17 /* 声明一个信号,只需声明,无需定义 */
18 void pushButtonTextChanged();
19
20 };
21 #endif // MAINWINDOW_H
第 16 至 18 行,声明一个信号 void pushButtonTextChanged(); ,可以看到信号无需public 等关键字修饰。
三、如何在项目中创建槽
创建槽的方法也很简单,也是直接在 mianwindow.h 里直接声明槽,在 mianwindow.cpp 里实现槽的定义, 声明槽必须写槽的定义( 定义指函数体的实现) ,否则编译器 编译时 将会报错。槽有以下特点:
- 槽可以是任何成员函数、普通全局函数、静态函数
- 槽函数和信号的参数和返回值要一致
我们声明了信号 void pushButtonTextChanged();所以我们声明的槽函数必须是无返回值类型 void,和无需参数。所以声明槽的代码如下。此外我们还声明一个 QPushButton 对象 pushButton。对象pushButton 可以写成简写 btn。这个根据个人习惯即可!简写的名称建议不要让人看不懂即可!同时还声明一个按钮点击的槽。
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
/* 引入 QPushButton */
#include <QPushButton>
//namespace Ui{ class MainWindow;}
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
signals:
/* 声明一个信号,只需声明 无需定义 */
void pushButtonTextChanged();
public slots:
/* 声明按键改变文本槽函数 */
void changeButtonText();
/* 声明按键点击的槽函数 */
void pushButtonClicked();
private:
// Ui::MainWindow *ui;
/* 声明一个对象pushButton */
QPushButton *pushButton;
};
#endif // MAINWINDOW_H
开始的namespace Ui可能让人有点摸不着头脑,这是因为qt把ui相关的类单独独立了出来,但类名相同,
声明namespace Ui是因为要调用Ui中的MainWindow,此MainWindow非彼MainWindow,后面涉及的*ui指针会调用它!
声明一个槽函数 void changeButtonText(); 。
声明了一个槽函数 void pushButtonClicked(); 。
声明了一个 QPushButton 对象 pushButton。
在 mainwindow.cpp 里 实 现 声 明 的 槽 函 数 void changeButtonText(); 和 和 void pushButtonClicked(); 。同时还实例化了 pushButton 对象。代码如下。
1 #include "mainwindow.h"
2
3 MainWindow::MainWindow(QWidget *parent)
4 : QMainWindow(parent)
5 {
6 /* 设置窗体的宽为 800,高为 480 */
7 this->resize(800,480);
8
9 /* 实例化 pushButton 对象 */
10 pushButton = new QPushButton(this);
11
12 /* 调用 setText()方法设定按钮的文本 */
13 pushButton->setText(" 我是一个按钮");
14 }
15
16 MainWindow::~MainWindow()
17 {
18
19 }
20
21 /* 实现按钮点击槽函数 */
22 void MainWindow::pushButtonClicked()
23 {
24 /* 使用 emit 发送信号 */
25 emit pushButtonTextChanged();
26 }
27
28 /* 实现按钮文本改变的槽函数 */
29 void MainWindow::changeButtonText()
30 {
31 /* 在槽函数里改变按钮的文本 */
32 pushButton->setText(" 被点 击了!");
33 }
第 7 行,设置程序窗体的大小,假若不设置,窗体为最小分辨率。这里设置宽为 800,高为 480。刚好是我们屏幕的分辨率。
第 10 行,实例化 pushButton 对象,在堆中实例化,并指定父对象为 this。因为我们在 mainwindow.h 里声明的是指针类型的对象。
实际上 QPushButton(this)的原型是 QPushButton(QWidget *parent = nullptr)。 在 C++基础里里就已经学过重载,实际上 QPushButton 类中还有下面两种初始化的方法。这也是 C++在 Qt 里重载的体现。
QPushButton(const QString &text, QWidget *parent = nullptr)
QPushButton(const QIcon &icon, const QString &text, QWidget *parent = nullptr)
在 22 行和 29 行里,实现了槽方法。
四、如何在项目中连接信号与槽
要在项目中连接信号与槽。完成连接的代码如下。
信号槽连接的代码如下。
connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
connect(this, SIGNAL(pushButtonTextChanged()), this, SLOT(changeButtonText()));
注意,发送信号的对象,和接收的信号的对象。因为我们 pushButtonClicked()是本类里定义的槽,所以用 this 来接收。同理,pushButtonTextChanged()也是本类定义的信号。所以发送者写成 this。changeButtonText()也是本类的槽函数,所以接收槽的对象也是 this。在 mainwindow.cpp 中信号槽连接的代码如下。
/* 信号与槽连接 */
connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
connect(this, SIGNAL(pushButtonTestChanged()), this, SLOT(changeButtonText()));
连接信号与槽,整个流程就是当点击了按钮,然后触发了 pushButtonClicked() , pushButtonClicked() 槽里发送pushButtonTextChanged() 信号, changeButtonText() 槽响应 pushButtonTextChanged() 信号,我们在 changeButtonText() 槽实现响应的动作(事件)。最终的实现效果是按钮的文本由“我是一个按钮”被点击时变成“被点击了!”。
编译程序及运行后,未点击按钮前如下。
点击按钮前:
点击按钮后:
上面的程序虽然简单,但是我们也最终实现了自定义的信号与槽
五、学会使用Qt类的信号与槽
Qt 里有大量的信号与槽,都是 Qt 自定义好的。基本够我们使用,如果没有找到想要的信号与槽,我们就可以按自定义信号与槽的方法去定义自己的信号和槽了。那么我们该如何使用 Qt 信号与槽呢?
想使用 Qt 的信号与槽,那么我们必须知道有哪些信号与槽。
connect(pushButton, SIGNAL(clicked()), this,SLOT(pushButtonClicked()));
按住 Ctrl 键,再点击 clicked(),进入 clicked()这个信号的定义处。
进入 QPushButton 的定义处,我们看到 QPushButton 不止 clicked 信号,还有其他信号,也有 QPushButton 的槽函数(返回上一步按 Alt + 方向左键)。在这里我们只是简单的看了如何在已知信号和槽里查找其他信号与槽。实际上在开发中我们经常需要使用 Qt 帮助文档来查看 Qt定义的信号与槽。我们将在下一章里讲解 Qt 帮助文档的使用,帮助文档里面就有如何查看 Qt的信号与槽等等。