一、信号(signal)与槽(slot)
在QT中,信号(signal)与槽(slot)机制是一种用于对象间通信的重要机制。它允许一个对象发出信号,而其他对象可以通过连接到该信号的槽来接收并处理这个信号。
信号是由QObject派生类中声明的特殊函数,用于表示某个事件的发生。例如,当按钮被点击时,QPushButton类会发出一个clicked()信号。每个信号都有一个唯一的名称,并且可以带有参数。
槽是普通的成员函数,用于接收和处理信号。槽函数可以在任何QObject派生类中定义,但必须使用特殊的宏进行声明。槽函数可以执行任意操作,包括修改对象的状态、调用其他函数等。
连接(connection)是将信号与槽关联起来的过程。通过连接,当信号发出时,与之连接的槽函数将被自动调用。连接可以在代码中手动创建,也可以使用Qt提供的可视化工具进行创建。
信号与槽机制的优势在于它们实现了松耦合的对象间通信。通过信号与槽,不同对象之间可以进行灵活的交互,而无需显式地引用彼此。这种机制使得代码更加模块化、可维护性更高,并且能够方便地实现事件驱动的程序设计。
二、元对象系统
在Qt中,元对象(Meta Object)是一种特殊的对象,用于存储类的元信息,包括类名、父类名、信号和槽等。每个继承自QObject的类都有一个对应的元对象。
元对象系统允许Qt实现一些高级功能,比如信号与槽机制、属性系统、动态属性以及对象反射等。通过元对象系统,Qt可以在运行时获取类的元信息,并且能够动态地操作这些信息。
元对象系统的核心是使用元对象宏(Q_OBJECT)来声明一个类为元对象类。当一个类被声明为元对象类时,Qt的元对象编译器(MOC)会生成额外的代码,用于处理信号与槽、属性等元信息。
通过元对象系统,Qt可以实现诸如信号与槽的自动连接、动态属性的添加与修改、对象类型的判断等功能。这使得Qt具有很强的灵活性和可扩展性,能够支持更加动态和智能的编程模式。
三、实现原理
在Qt中,信号(signal)与槽(slot)机制是元对象系统的核心组成部分。
首先,元对象系统通过使用元对象宏(Q_OBJECT)来声明一个类为元对象类。当一个类被声明为元对象类时,Qt的元对象编译器(MOC)会生成额外的代码,用于处理信号与槽、属性等元信息。
然后,信号与槽机制是元对象系统的一种实现方式。通过元对象系统,Qt可以在运行时获取类的元信息,并且能够动态地操作这些信息。而信号与槽机制则是利用了元对象系统的特性,实现了对象间的松耦合通信。
具体来说,当一个类声明了信号时,MOC会将信号的相关信息添加到该类的元对象中。而当一个类声明了槽时,MOC会将槽的相关信息添加到该类的元对象中。通过连接信号与槽,可以在运行时实现信号的发出和槽的自动调用。
因此,信号与槽机制依赖于元对象系统,通过元对象系统提供的元信息,实现了对象间的动态通信。元对象系统为信号与槽机制提供了基础,使得信号与槽能够在运行时进行连接和调用。
四、设计模式
在Qt中,信号与槽机制的底层实现使用了观察者模式(Observer Pattern)和命令模式(Command Pattern)。
观察者模式用于实现信号与槽之间的连接和通信。在观察者模式中,信号充当了被观察者(Subject),而槽函数充当了观察者(Observer)。当信号发出时,所有连接到该信号的槽函数都会被调用,实现了对象间的松耦合通信。
命令模式用于将信号与槽的调用进行封装。在命令模式中,信号的发出相当于命令的发出,而槽函数相当于命令的接收者。通过将信号与槽的调用封装成命令对象,可以实现对调用的延迟、撤销等操作。
除了观察者模式和命令模式,Qt中还使用了其他设计模式来支持信号与槽机制的实现。例如,Qt中的元对象系统使用了元数据模式(Metadata Pattern)来存储和处理类的元信息。此外,Qt还使用了工厂模式(Factory Pattern)来创建信号与槽的连接。
五、使用示例
1、使用可视化工具连接
在 UI 界面中右键按钮控件,选择【Go to slot】,可以看到该按钮控件下的信号,我们选择单击信号【clicked】,编辑器会帮我们自动在头文件、源文件添加对应的槽函数,我们只需要实现处理逻辑,
private slots:
void on_pushButton_clicked();
2、使用 connect 函数
使用 connect 函数,将按钮 pushButton_2 的单击信号 clicked 连接到本窗口 this 的槽函数 on_pushButton_clicked,因此点击按钮2的效果等同于按钮1。
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 谁发出信号,发出什么信号,谁接收信号,谁处理信号?
// 使用 connection 与宏
// connect(ui->pushButton_2,SIGNAL(clicked(bool)),this,SLOT(on_pushButton_clicked()));
// 使用 connection 与指针
// connect(ui->pushButton_2,&QPushButton::clicked,this,&MainWindow::on_pushButton_clicked);
// 使用 connection 与匿名函数(当信号的处理逻辑非常简单时,使用匿名函数为佳)
connect(ui->pushButton_2,&QPushButton,[this](){
qDebug() << "click";
});
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::on_pushButton_clicked()
{
qDebug() << "click";
}
3、自定义信号与槽
我们自定义一个遥控器类RemoteControl与空调类AirConditioner,遥控器发出信号,空调根据指令运行,
注意创建类时勾选 Q_OBJECT 宏,接下来添加遥控器的信号,
#ifndef REMOTECONTROL_H
#define REMOTECONTROL_H
#include <QObject>
class RemoteControl : public QObject
{
Q_OBJECT
signals:
// 默认制冷
void run();
// 制热/抽湿...
void run(QString mode);
public:
RemoteControl();
};
#endif // REMOTECONTROL_H
可以看到我们重载了信号 run,默认情况下 run 没有参数,属于制冷模式,其他情况 run 根据参数 mode 运行不同的模式。
然后我们为空调添加槽函数,同样重载了槽函数 exec 实现运行不同模式,注意这里的重载参数必须与信号保持一致,
#ifndef AIRCONDITIONER_H
#define AIRCONDITIONER_H
#include <QObject>
class AirConditioner : public QObject
{
Q_OBJECT
public:
AirConditioner();
public slots:
void exec();
void exec(QString mode);
};
#endif // AIRCONDITIONER_H
#include "airconditioner.h"
#include <QDebug>
AirConditioner::AirConditioner() {}
void AirConditioner::exec()
{
qDebug()<<"cold";
}
void AirConditioner::exec(QString mode)
{
qDebug()<< mode;
}
最后我们在 mainwindow 实例化一个遥控器跟空调对象,将信号与槽函数连接起来,
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QDebug>
#include "airconditioner.h"
#include "remotecontrol.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
AirConditioner airConditioner;
RemoteControl remoteControl;
connect(&remoteControl,SIGNAL(run()),&airConditioner,SLOT(exec()));
connect(&remoteControl,SIGNAL(run(QString)),&airConditioner,SLOT(exec(QString)));
// emit 可省略
emit remoteControl.run();
remoteControl.run("hot");
}
MainWindow::~MainWindow()
{
delete ui;
}