一、Qt理论知识简记
(一)信号与槽[1]
信号与槽是Qt编程的基础,其使得处理界面上各个组件的交互操作变得比较直观和简单,GUI(Graphical User Interface)程序设计的主要工作就是对界面上各组件的信号进行相应。信号(signal)是在特定情况下被发射的通知;槽(slot)是对信号进行响应的函数,槽函数与一般的C++函数一样,可以具有任何参数,也可以被直接调用。
信号与槽关联是基于函数QObject::connect()实现的,其可以通过SIGNAL/SLOT(Qt的宏)表现,也可以通过函数指针表现,具体示例如下:
cpp
# SIGNAL/SLOT(Qt宏)形式
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection);
一般用法如下:
connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
# 函数指针形式(信号名称唯一,不存在参数不同的其它同名信号)
QMetaObject::Connection QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection);
一般用法示例如下:
connect(sender, &sender::signal, receiver, &receiver::slot);
在使用信号与槽时,需要注意的规则:
在使用信号与槽的类中,必须在类的定义中插入宏Q_OBJECT;
当信号和槽函数带有参数时,在函数connect()里要指明各参数的类型,但不用指明参数名称;
当一个信号与多个槽函数关联时,槽函数按照建立连接时的顺序依次进行;
多个信号可以连接同一个槽函数;
一个信号可以连接另一个信号;
当一个信号被发射时,与其关联的槽函数通常被立即执行。只有当信号关联的所有槽函数运行完毕后,才运行发射信号处后面的代码。
(二)元对象系统[1][4][5]
Qt中引入了元对象系统(Meta-Object System)对标准C++语言进行扩展,增加了信号与槽、属性系统、动态翻译等特性,为编写GUI应用程序提供了极大的方便。Qt的元对象系统的功能建立在以下三个方面:
QObject类是所有使用元对象系统的类的基类;
必须在一个类的开头部分插入宏Q_OBJECT,这样这个类才可以使用元对象系统的特性;
在构建项目时,MOC会读取C++源文件,当它发现类的定义里有Q_OBJECT宏时,它就会为这个类生成另一个包含元对象支持代码的C++源文件,这个生成的源文件连同类的实现文件一起被标准C++编译器编译和链接(MOC为每个QObject的子类提供必要的代码来实现元对象系统的特性)。
元对象系统的特性是通过QObject的一些函数来实现的。元对象系统的特性包括元对象(meta-Object)、类型信息、动态翻译、对象树(Object-Tree)、信号与槽和属性系统(类的定义代码中需要用宏Q_PROPERTY定义属性)。
由于C++的RTTI机制只能提供有限的类型信息,于是Qt构建了自己的元对象系统,并基于元对象信息实现了强大的信号槽机制[4]。
简略补充说明(详细可见参考资料[1]-P48):
对象树。使用QObject及其子类创建的对象(统称为QObject对象)是以对象树的形式来组织的。创建一个QObject对象时若设置一个父对象,它就会被添加到父对象的子对象列表里。一个父对象被删除时,其全部子对象就会被自动删除。QObject类的构造函数里有一个参数parent,用于设置对象的父对象。
元对象。每个QObject及其子类的实例都有一个自动创建的元对象,元对象是QMetaObject类型的实例。元对象存储了类的实例所属类的各种元数据,包括类信息元数据、方法元数据、属性元数据等。所以,元对象实质上是对类的描述。
通过使用QObject和QMetaObject提供的接口函数,可以在运行时获得一个对象的类名称以及其父类的名称,判断其是否从某个类继承而来。由于这个元对象系统的特性,实现运行时类型信息获取并不需要C++编译器的运行时类型信息(run-time type information,RTTI)支持。
二、Qt信号槽机制下的信号与信号槽的连接方式
枚举类型Qt::ConnectionType包含四个值,分别为Qt::AutoConnection(默认值)、Qt::DirectConnection、Qt::QueuedConnection和Qt::BlockingQueuedConnection。
Qt::AutoConnection(默认值),如果信号的接收者与发射者在同一个线程中,就使用Qt::DirectConnection方式,否则使用Qt::QueuedConnection方式,在信号发射时自动确定关联方式;
Qt::DirectConnection,信号被发射时槽函数立即运行,槽函数与信号在同一个线程中;
Qt::QueuedConnection,在事件循环回到接收者线程后运行槽函数,槽函数与信号在不同的线程中;
Qt::BlockingQueuedConnection,与Qt::QueuedConnection相似,区别是信号线程会阻塞,直到槽函数运行完毕。当信号与槽函数在同一个线程中时绝对不能使用这种方式,否则会造成死锁。
图1 QT信号槽机制下的信号与信号槽的连接方式
三、带参信号传递与带参槽函数接收
(一)信号与槽位于同一个线程
1、参数为Qt元对象系统中的基础类型
cpp
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
signals:
void emitQString(QString,int);
public slots:
void showText(QString,int);
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->pushButton,&QPushButton::clicked,this, [=]()
{
emit emitQString("My age is",18);
});
connect(this,SIGNAL(emitQString(QString,int)),this,SLOT(showText(QString, int)));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::showText(QString, int)
{
int age = 18;
ui->label->setText("My age is"+QString::number(age));
}
// main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
图2 信号与槽位于相同线程的Qt基础类型参数传递实验结果
2、参数为自定义类型(Qt元对象系统不能自然识别)
cpp
// mainwindows.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
struct MyStruct
{
MyStruct::MyStruct(QString aa,int bb,int *cc):a(aa),b(bb),c(cc){};
QString a;
int b;
int *c;
};
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
signals:
void emitQString(MyStruct);
public slots:
void showText(MyStruct);
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
connect(ui->pushButton,&QPushButton::clicked,this, [=]()
{
int *a = new int(12);
MyStruct s = MyStruct("My age is",6,a);
emit emitQString(s);
delete a;
});
connect(this,SIGNAL(emitQString(MyStruct)),this,SLOT(showText(MyStruct)));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::showText(MyStruct s)
{
int age = s.b + *(s.c);
ui->label->setText(s.a+QString::number(age));
}
// main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
图3 信号与槽位于相同线程的自定义结构体类型参数传递实验结果
(二)信号与槽位于不同线程[2]
1、参数为Qt元对象系统中的基础类型
cpp
// MyThread_1.h
#ifndef MYTHREAD_1_H
#define MYTHREAD_1_H
#include <QThread>
class MyThread_1:public QThread
{
Q_OBJECT
public:
explicit MyThread_1(QObject *parent = nullptr);
protected:
void run();
signals:
void emitString(QString, int);
};
#endif // MYTHREAD_1_H
// MyThread_1.cpp
#include "MyThread_1.h"
MyThread_1::MyThread_1(QObject *parent) : QThread(parent)
{
}
void MyThread_1::run()
{
int age = 18;
QString a = "My age is";
emit emitString(a,age);
}
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
signals:
public slots:
void showText(QString,int);
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "MyThread_0.h"
#include "MyThread_1.h"
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
MyThread_1* subThread = new MyThread_1;
connect(ui->pushButton_2,&QPushButton::clicked, this, [=]()
{
subThread->start();
});
connect(subThread,SIGNAL(emitString(QString,int)),this,SLOT(showText(QString,int)));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::showText(QString text,int age)
{
ui->label_2->setText(text+QString::number(age));
}
// main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
图4 信号与槽位于不同线程的Qt基础类型参数传递实验结果
2、参数为自定义类型(Qt元对象系统不能自然识别)
2.1 未注册元对象类型直接使用
由图5可知,自定义结构体类型无法直接作为信号参数传递到位于不同线程中的槽函数中,不同线程间的信号和槽通过Qt::QueuedConnection 类型连接。程序报错为"QObject::connect: Cannot queue arguments of type 'MyStruct'(Make sure 'MyStruct' is registered using qRegisterMetaType().)"。
图5 信号与槽位于不同线程的自定义结构体类型参数传递实验结果
2.2 注册元对象类型使用
在注册元对象类型时,首先需要在自定义结构体定义后接上"Q_DECLARE_METATYPE(自定义结构体名称);",然后在该信号connect前加入"int id = qRegisterMetaType<自定义结构体名称>();"以完成元对象类型注册。
需要注意的是,自定义结构体中需要有合适的默认构造函数可用。一般而言不重新定义构造函数,即使用默认构造函数。
图6 Qt Assistant示例(源自参考资料[2])
cpp
// MyThread_1.h
#ifndef MYTHREAD_1_H
#define MYTHREAD_1_H
#include <QThread>
struct MyStruct
{
QString a;
int b;
int *c;
};
Q_DECLARE_METATYPE(MyStruct);
class MyThread_1:public QThread
{
Q_OBJECT
public:
explicit MyThread_1(QObject *parent = nullptr);
protected:
void run();
signals:
void emitString(MyStruct);
};
#endif // MYTHREAD_1_H
// MyThread_1.cpp
#include "MyThread_1.h"
MyThread_1::MyThread_1(QObject *parent) : QThread(parent)
{
}
void MyThread_1::run()
{
int *a = new int(12);
MyStruct s;
s.a = "my age is";
s.b = 6;
s.c = a;
emit emitString(s);
delete a;
}
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <MyThread_1.h>
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
signals:
public slots:
void showText(MyStruct s);
private:
Ui::MainWindow *ui;
};
#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "MyThread_1.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
MyThread_1* subThread = new MyThread_1;
connect(ui->pushButton_2,&QPushButton::clicked, this, [=]()
{
subThread->start();
});
int id = qRegisterMetaType<MyStruct>();
connect(subThread,SIGNAL(emitString(MyStruct)),this,SLOT(showText(MyStruct)));
}
MainWindow::~MainWindow()
{
delete ui;
}
void MainWindow::showText(MyStruct s)
{
int age = s.b + *(s.c);
ui->label_2->setText(s.a+QString::number(age));
}
// main.cpp
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
图7 信号与槽位于不同线程的自定义结构体类型参数传递实验结果
参考资料:
[1] Qt 6 C++开发指南 / 王维波,栗宝鹃,侯春望著.---北京:人民邮电出版社,2023.1.
[2] Assistant 5.15.2 (MSVC 2019 64-bit)-Qt::ConnectionType(Qt使用文档助手)
[4] Qt中的元对象系统(Meta-Object System) - 知乎 (zhihu.com)
[5] 【Qt 元对象系统04】 深入浅出Qt的QMetaObject:探索元对象的魔法-阿里云开发者社区 (aliyun.com)