前言:这篇就来认识Qt中的信号与槽。
1.从控件到信号与槽
显示一段Hello world,并不只有标签这一种方式。Qt提供了不少和文字有关的控件,先简单区分一下:
| 控件 | 主要用途 |
|---|---|
QLabel |
显示文本、图片等内容 |
QLineEdit |
单行文本输入 |
QTextEdit |
多行文本编辑 |
QPushButton |
普通按钮,文字通常作为按钮的说明 |
比如在窗口构造函数中,可以通过下面两行代码创建一个单行输入框:
cpp
QLineEdit* edit = new QLineEdit(this);
edit->setText("Hello world!");
使用时包含<QLineEdit>。new QLineEdit(this)创建输入框,并把当前窗口设置为父对象;setText()设置其中的文字。这和前面创建标签的思路基本一致。
这些控件都直接或间接继承自QWidget,而QWidget又继承自QObject。可以先看下面这张简化关系图:

其实还有一些中间基类,比如QPushButton和QRadioButton还经过QAbstractButton,这里主要是认识它们都在这个继承体系中,能够使用Qt提供的对象通信机制。
控件有了,接下来就需要考虑交互。比如用户点了一下按钮,程序应该修改文字,还是关闭窗口?按钮本身并不知道我们的业务要求,这时就需要把用户操作与处理代码联系起来。
信号可以理解成对象发出的一条通知,槽则是用来响应这条通知的函数。
对于按钮,clicked表示发生了点击;我们再准备一个处理函数,让Qt在收到这个信号时调用它。建立这层关系的函数,就是connect()。
这里先关注四件事:哪个对象发出信号、发出什么信号、由哪个对象接收、接收后执行什么函数。发送者和接收者不一定都是界面控件,本篇先用按钮与窗口来理解。
Qt中的信号与槽属于对象之间的通信机制,和Linux中的进程信号不是同一套东西。
要让某次点击触发我们的处理函数,需要在点击发生前建立连接。
2.connect的基本使用
2.1点击按钮关闭窗口
先看一个直接的例子:窗口上放一个按钮,点击它就关闭当前窗口。
使用QPushButton需要包含<QPushButton>。下面是构造函数中负责这一功能的部分,放在ui->setupUi(this);之后:
cpp
// 设置位置
QPushButton* button = new QPushButton(this);
button->setText("我是按钮");
button->move(200, 300);
// close 是 QWidget 内置的槽函数,Widget 继承 QWidget 所以可以使用
connect(button, &QPushButton::clicked, this, &Widget::close);
前三行完成按钮的创建、文字设置和位置设置。move(200, 300)把按钮放到相对于父控件的这个位置
真正把点击和关闭联系起来的是最后一行。把四个参数拆开看:
| 参数 | 在这里表示什么 |
|---|---|
button |
信号发送者,也就是这个按钮对象 |
&QPushButton::clicked |
要连接的点击信号 |
this |
信号接收者,也就是当前Widget窗口对象 |
&Widget::close |
接收后调用的成员函数,这里用于关闭窗口 |
连起来就是:当button发出clicked信号时,调用当前窗口的close()。
这里使用的是QObject提供的静态connect()重载。我们的Widget继承了QWidget,而QWidget属于QObject的继承体系,所以可以在成员函数中直接这样调用。
connect()执行时是在建立联系。后面按钮真正发出信号,才会触发相应处理。
再看第二、第四个参数:它们是成员函数指针,用来告诉Qt要连接哪一个信号、哪一个处理函数。
Widget是我们自己定义的窗口类,QWidget才是Qt提供的类。close()虽然不是我们在Widget里重新实现的,但它从基类继承而来,所以可以写&Widget::close。
还有一点,关闭窗口与立即删除窗口对象不是一回事。对于这里没有额外修改关闭行为的窗口,close()会关闭并隐藏它;对象什么时候析构,还要看它的生命周期和相关设置。可以查考QWidget的close说明。
2.2clicked
刚开始写这行代码时,还有一个很容易混淆的地方:补全列表里既有click,又有clicked,它们有什么区别?

click()是一个槽函数,可以通过代码调用它,模拟一次按钮点击;clicked则是按钮被点击时发出的信号。我们希望响应点击,所以connect()的第二个参数使用的是clicked。
如果在QPushButton自己的接口列表中没有直接找到clicked,可以继续看它的父类QAbstractButton。Qt把不同按钮共有的功能放到了这个基类中,clicked就是继承来的信号。QAbstractButton的clicked说明
查文档时,除了看名字,还要看这个信号什么时候发出 、参数表达什么含义,以及相关功能是不是从父类继承的。把这些弄清楚,比只记住一行connect()更有用,但现在有了AI确实也更方便,但想要更准确权威的答案还是得要参考官方提供的(可能再过几年就不一定了)。
3.通过槽函数切换按钮文字
关闭窗口使用的是Qt已经提供好的函数。如果想实现自己的处理逻辑实现一些定制化的需求,例如让按钮文字在Hello world与Hello Qt之间切换,就可以自己写一个函数。
下面分成纯代码和Designer两种方式。它们是同一个效果的不同写法,分别使用即可,不要把关闭窗口和切换文字的几组示例直接叠在一起。
3.1通过代码创建按钮
先看窗口类的声明,也就是widget.h:
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include <QPushButton>
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
Widget(QWidget *parent = nullptr);
~Widget();
void HandleClick();
private:
Ui::Widget *ui;
QPushButton* MyButton;
};
#endif // WIDGET_H
这里主要关注新增的两项:HandleClick()是我们自己的点击处理函数,MyButton用来保存按钮的地址,让另一个成员函数也能够访问这个按钮。
前面关闭窗口的示例中,按钮指针只是构造函数里的局部变量,因为建立连接后,不需要再通过另一个成员函数读取它。现在HandleClick()要修改按钮文字,所以这里把指针保存为窗口类的成员。
保留构造函数已有的ui(new Ui::Widget)初始化和ui->setupUi(this);,把前面创建关闭按钮的那几行替换为下面这组代码:
cpp
MyButton = new QPushButton(this);
MyButton->setText("Hello world");
connect(MyButton, &QPushButton::clicked, this, &Widget::HandleClick);
这次信号发送者变成了MyButton,接收者仍然是当前窗口,只是最后一个参数换成了&Widget::HandleClick。
接着在widget.cpp中实现处理函数:
cpp
void Widget::HandleClick()
{
if (MyButton->text() == QString("Hello world")){
MyButton->setText("Hello Qt");
}else{
MyButton->setText("Hello world");
}
}
text()用来取得按钮当前显示的文字,setText()则用来修改文字。第一次点击时,原来的文字是Hello world,于是进入if分支,把它改成Hello Qt;再点击一次,就进入else分支,改回Hello world。
整个过程就是:按钮被点击,发出clicked信号,Qt调用当前窗口的HandleClick(),这个函数再修改按钮文字。
3.2通过Designer创建按钮
也可以在Designer中直接拖入一个QPushButton。这样按钮的创建代码由界面生成流程负责,我们只需要找到这个按钮,再建立连接。
先在widget.ui中放入一个按钮,把它的objectName设为pushButton,初始text设为Hello world。
这两个属性很容易混在一起,实际上用途不同:
| 属性 | 用途 |
|---|---|
objectName |
对象名称,界面生成代码会用相应名称提供访问成员 |
text |
按钮在界面上显示的文字 |
按钮上显示Hello world,不代表代码中要写ui->Hello world。如果它的objectName是pushButton,对应的访问方式就是ui->pushButton。
这里再把界面生成过程接起来看:构建时,uic读取widget.ui,生成ui_widget.h;其中的界面辅助类保存控件指针,并提供setupUi()来创建和设置控件。qmake负责组织这一步构建工作,真正把.ui转换成C++代码的是uic。UI文件的使用方式
new Ui::Widget创建的是辅助对象,随后调用ui->setupUi(this),才会完成界面控件的创建。因此,下面这行连接要放在ui->setupUi(this);之后:
cpp
connect(ui->pushButton, &QPushButton::clicked, this, &Widget::HandleClick);
这一版不需要再执行MyButton = new QPushButton(this);那组三行,也不用再次手动创建同一个按钮。widget.cpp仍然需要包含"ui_widget.h",才能访问界面辅助类中的成员。
cpp
void Widget::HandleClick()
{
if (ui->pushButton->text() == QString("Hello world")){
ui->pushButton->setText("Hello Qt");
}else{
ui->pushButton->setText("Hello world");
}
}
这里仍然是在同一个HandleClick()中做文字切换,变化的只是访问按钮的方式。前一版通过我们自己保存的MyButton访问,这一版通过界面辅助对象中的ui->pushButton访问。
对于比较固定的界面,用Designer安排控件通常比较方便;需要动态创建控件时,直接写代码也很自然。这两种方式可以结合使用,最后创建出来的仍然是Qt控件,信号与槽的连接思路没有变化, 哪个方便就用哪个。
4.connect的两种写法
4.1SIGNAL与SLOT写法
查看Qt文档或者一些已有代码时,还会遇到另一种connect()形式。它的第二、第四个参数是const char*,看起来和前面传入的成员函数指针完全不同:

这张图展示的是字符串形式的连接重载。发送者、信号、接收者、处理函数这四个要素仍然存在,但信号与槽的表达方式不同。
仍然以点击按钮关闭窗口为例,对照写法如下:
cpp
connect(button, SIGNAL(clicked(bool)), this, SLOT(close()));
这里的SIGNAL()和SLOT()是宏。宏里面写的是函数名和参数类型,比如clicked(bool)、close(),不写类名、返回类型,也不写&QPushButton::clicked这样的成员函数指针表达式。
它们会把相应的函数签名文字转换成Qt需要的字符串形式,再由Qt查找对应的信号与槽。这不是把一个函数指针强行转换成char*。 字符指针与成员函数指针是不同类型,两种写法能够工作,是因为调用了不同的connect()重载。
4.2成员函数指针写法
从Qt 5开始,可以使用前面一直在写的这种形式:
cpp
connect(button, &QPushButton::clicked, this, &Widget::close);
旧的宏形式仍然可以使用,只是这里更适合通过成员函数指针,把对象与接口直接对应起来。
如果继续看QObject::connect的相关实现,就会发现这种重载使用了模板:

Func1、Func2分别对应传入的信号和处理函数类型,QtPrivate::FunctionPointer用于提取相关类型信息。这里先看懂它为什么能够接收不同类的成员函数即可,因为无我也没去研究。
后面继续补充信号和槽
完