目录
QGuiApplication、QCoreApplication、QApplication
Qt
分析QObject
QObject类是Qt框架中的核心基类,也就是说几乎所有的Qt对象都直接或者间接的继承了QObject,其提供了Qt对象模型的基础功能(例如对象树管理、信号和槽机制、事件处理和元对象系统)
Qt对象的基本功能简要叙述
- 对象树结构:QObject维护一个对象树,每个QObject都有一个父对象,子对象的生命周期由父对象管理。父对象销毁,那么其下的所有子对象也会被销毁
- 信号与槽机制:用于对象间的通信。信号是对象发出的消息,槽则是响应信号的函数
- 事件处理:灵活的事件处理机制,可以通过重写event或者特定的事件处理函数,实现各种事件的响应(简单来说就是一个行为触发什么反应)
- 元对象系统:支持运行时类型信息、属性系统、动态属性、信号和槽的自动连接等功能。
对象树结构
父子关系
- 父对象:每个QObject类都可以指定一个父对象,然后构成层次化的对象树
- 子对象:父对象可以拥有多个子对象,使用children()函数获取
生命周期管理
- 自动销毁:父对象销毁的时候,所有挂在它下面的子对象也会被销毁
- 内存管理:减少了手动管理内存的负担,防止内存泄漏
元对象系统
功能分析
- 运行时候的类型信息:获取对象的类型、父类、方法列表等信息
- 属性系统:支持动态属性的增读改
- 信号与槽自动连接:对象创建的时候,自动连接名称匹配的信号与槽
关键函数分析
QMetaObject
:包含类的元数据信息,如类名、信号、槽、属性等QMetaProperty
:表示属性的元数据信息QMetaMethod
:表示方法(包括信号和槽)的元数据信息
// 获取对象的类名
QObject *obj = new QObject();
qDebug() << obj->metaObject()->className(); // 输出 "QObject"
//动态属性
obj->setProperty("dynamicProperty", 123);
int value = obj->property("dynamicProperty").toInt();
//方法调用
QMetaObject::invokeMethod(obj, "methodName", Q_ARG(int, 42));
QObject其他重要特性分析
字符串翻译,专门用于支持应用程序国际化
QString text = tr("Hello, World!");
定时器功能,启动定时器(startTimer ( int interval)),处理定时器事件,重写timerEvent(QTimerEvent*event)函数处理定时器事件
class MyObject : public QObject {
Q_OBJECT
public:
MyObject() {
startTimer(1000); // 每隔1秒触发一次
}
protected:
void timerEvent(QTimerEvent *event) override {
qDebug() << "Timer triggered";
}
};
跨线程处理问题,也就是把一个线程的QObject类放入其他线程中
QThread *thread = new QThread();
MyObject *obj = new MyObject();
obj->moveToThread(thread);
thread->start();
项目实践中需要注意事项
- Q_OBJECT宏
- 作用:该宏是元对象系统工作的基础,必须在定义了信号、槽或者使用元对象功能类中添加这个宏
- 构建系统编译的时候需要对CMake进行配置
- 多重继承
- 因为C++内部菱形继承的问题,QObject不支持多重继承
- 需要通过组合(成员对象)而不是继承来实现多重继承
- 线程安全
- 大多数的QObject操作都不是线程安全的,所以需要尽力避免在同一个线程中访问同一个QObject,必要的时候使用信号和槽进行线程间通信
QGuiApplication、QCoreApplication、QApplication
继承关系分析
QObject
└── QCoreApplication
└── QGuiApplication
└── QApplication
QCoreApplication
主要用于不需要图形用户的界面的控制台或者后台应用程序,主要功能有事件循环、信号和槽机制、对象树管理、应用程序级别的控制等
#include <QCoreApplication>
#include <QDebug>
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
qDebug() << "This is a console application.";
return app.exec();
}
QGuiApplication
主要用于需要GUI支持但是不使用Qt Widgets的应用程序,主要功能就是图形用户界面事件处理、窗口系统集成、输入法管理等
#include <QGuiApplication>
#include <QQuickView>
int main(int argc, char *argv[]) {
QGuiApplication app(argc, argv);
QQuickView view;
view.setSource(QUrl("qrc:/main.qml"));
view.show();
return app.exec();
}
QApplication
提供完整的GUI应用程序功能,其支持Qt Widgets模块,适用于桌面应用程序、以及复杂的GUI应用
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QPushButton button("Hello, Qt Widgets!");
button.resize(200, 100);
button.show();
return app.exec();
}
信号和槽概述
用户和空间之间的一次交互称为一个事件,每个事件会发出一个信号,例如点击窗口最大化按钮,可以调整窗口为最大。
Qt中所有的控件都是具有接收信号的能力,一个控件可以接收多个信号,不同的信号响应不同的动作。Qt中,对信号做出的响应动作就是槽。
信号和槽是Qt的信息传输机制,负责将相互独立的控件关联起来
QT中的信号和槽机制,以及如何实现松耦合
- 信号与槽是Qt中用于对象间通信的核心机制:信号是由对象发出消息,槽则是接收信号并执行某些操作的函数。当一个信号发出,所有连接到该信号的槽函数都会被调用
- 松耦合:由于信号和槽通过 QObject: : counnect()方法进行连接,发出信号的对象并不知道接收信号的对象及其具体实现,按照这种规则,实现对象之间的解耦合,从而增强了代码的可维护性和可拓展性
信号和槽的连接类型以及其区别
- Qt::AutoConnection
- 默认连接类型,QT会自动判断信号发出者和接受者是否存在于同一个线程中,如果在同一个线程中,则直接连接;如果不在同一个线程,则使用队列连接
- Qt::DirectConnection
- 信号发出时,槽函数立即会被调用
- Qt::QueuedConnection
- 信号发出后,槽函数会被放入到接收者线程的事件队列中,由于事件循环负责调用,适用于跨线程通信
- Qt::BlockingQueueConnection
- 多线程环境下使用,发出信号的线程会被阻塞,直到槽函数执行完毕
- Qt::UniqueConnection
- 确保信号和槽之间的连接是唯一的,如果试图建立重复的连接,将会失败
信号的本质
Qt中信号本质上是由Qt元对象系统支持的一种事件通知机制,使用了元对象编译器生成的代码和运行时候的元数据信心。该处的信号与传统C++函数不一样,而是通过一种特定机制(内部元对象系统和元对象编译器支持的一种机制),用于对象之间松耦合的同时,类型安全的通信。
信号实现细节
- 信号声明
- 在类中通过signals关键字声明信号,只是在类中声明而不通过具体函数实现
- 这些声明可以被MOC识别(源对象编译器),用于生成额外的代码支持
- 元对象编译器处理
- 元对象编译器会读取头文件,然后解析signals和slots等关键字,生成对应的元信息代码
- 为每个QObject派生类生成一个静态的QMetaObject实例,其中包含类名、信号、槽、属性等元数据信息
- 信号发射
- 使用emit关键字来发射信号,实际上emith是一个空的宏,作用就是增强代码的可读性
- 发射信号的时候,调用的就是由MOC生成的信号实现函数
- 信号内部机制
- 信号函数会调用QMetaObject::active(),该函数是QT内部专门用于处理信号和槽连接的核心函数
- activate()函数则是主要根据信号的ID,在连接列表中找到所有连接的槽函数,然后依次调用它们
- 槽函数调用
- 如果槽函数在同一个线程中,直接调用槽函数,实现同步调用
- 如果槽函数在其他线程中,调用会被封装为事件,投递到目标线程的事件队列中,实现异步调用
Qt中跨线程通信理解
首先QT系统会自动处理线程间的信号和槽调用,因为在其内部处理机制中,如果发现信号和槽函数不是在同一个线程中,QT就会自动将信号发射封装到事件中,然后将其放入到目标线程的事件循环中
其次跨线程是线程安全的,通过该机制确保了跨线程的信号和槽调用时线程安全的,从而简化多线程编程的难度
槽的本质
Qt中的槽本质就是普通的成员函数,但是通过slots关键字和Qt的元对象系统,槽函数被纳入了信号和槽的机制中。
只有当信号发射的时候,槽函数会根据连接关系被动态调用,实现了对象之间的松耦合以及事件的响应机制,这样也就简化了编程。
槽的核心
- 成员函数角度:槽本身就是一个普通的C++成员函数,可以像调用其他成员函数一样调用它,同样也可以重载等一系列操作
- 事件响应机制:也就是当一个信号发射的时候,与槽连接的函数会被自动调用,这种机制允许对象在不直接调用另一个对象方法的情况下,通知它某些事情已经发生
- 松耦合设计:信号和槽的设计,两者不需要知道彼此的存在,从而增强了代码的模块性以及可维护性
- 线程安全:Qt的信号和槽机制是支持跨线程通信的,其是通过事件队列实现的。当一个信号从一个线程发射的时候,连接的槽在另一个线程中时,Qt会自动将槽的调用封装成一个事件并将其发送到目标线程的事件队列中,从而确保线程安全
信号与槽的使用
连接信号和槽
信号与槽使用机制分析
- 定义信号和槽
- 分别定义一个信号以及一个对应的槽函数slots
- 连接信号和槽
- Connect函数
- 发射信号
QObject::connect()函数--下面详细说明Qt5版本之后的情况
// 使用函数指针
QObject::connect(sender, &SenderClass::signalName, receiver, &ReceiverClass::slotName);
// 使用 Lambda 表达式
QObject::connect(sender, &SenderClass::signalName, [=](parameters){
// 槽函数的实现
});
参数说明
- sender:发出信号的对象(信号是谁发的)
- signalName:信号名字,该处使用的是函数指针
- receiver:接收信号并执行槽函数的对象(注意是QObject类型)
- slotName:槽函数名字,该处也是函数指针,用于发出对象触发后的执行逻辑
新语法的使用事例
connect(button, &QPushButton::clicked, this, &MainWindow::handleButton);
connect(button, &QPushButton::clicked, this, [=](){
// 在此处理按钮点击事件
});
信号与槽的参数匹配
void signalExample(int a, QString b);
void slotExample(int a);
connect(sender, &Sender::signalExample, receiver, &Receiver::slotExample);
- 信号的参数可以多余槽函数的参数,但是多余的参数是会被忽略的
- 信号和槽的参数类型必须是严格匹配的,或者可以隐形转换
连接类型
connect(sender, &Sender::signalName, receiver, &Receiver::slotName, Qt::QueuedConnection);
最后一个参数可以指定其连接类型,一般是使用默认连接方式
Qt::AutoConnection
(默认):如果发送者和接收者在同一线程,采用直接连接;否则,采用队列连接Qt::DirectConnection
:信号发出时,立即调用槽函数(在发送者的线程中执行)Qt::QueuedConnection
:槽函数会被放入接收者所在线程的事件队列中,等待执行Qt::BlockingQueuedConnection
:类似于队列连接,但发送者会阻塞,直到槽函数执行完毕(需慎用,可能导致死锁)
完整使用流程事例
- 定义信号与槽函数
// 在头文件中
class MyButton : public QPushButton {
Q_OBJECT
public:
explicit MyButton(QWidget *parent = nullptr);
signals:
void mySignal(int value);
public slots:
void mySlot(int value);
};
- 实现信号与槽函数
// 在源文件中
MyButton::MyButton(QWidget *parent) : QPushButton(parent) {
connect(this, &MyButton::clicked, this, [=](){
emit mySignal(42);
});
}
void MyButton::mySlot(int value) {
qDebug() << "Received value:" << value;
}
- 连接信号和槽函数
MyButton *button = new MyButton(this);
connect(button, &MyButton::mySignal, this, &MainWindow::mySlot);
查看内置信号和槽
内置信号概述
class QPushButton : public QAbstractButton {
Q_OBJECT
public:
// 构造函数和其他成员
signals:
void clicked(bool checked = false);
void pressed();
void released();
void toggled(bool checked);
};
clicked信号
当用户完成一次的点击操作,也就是按下+释放按钮后,clicked信号就会被发射,其中的参数bool checked表示按钮的当前选中状态,主要用于可切换按钮
普通按钮
// 创建一个普通按钮
QPushButton *button = new QPushButton("Click Me", this);
// 连接 clicked 信号到槽函数
connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClicked);
// 槽函数定义
void MainWindow::onButtonClicked(bool checked) {
qDebug() << "Button clicked. Checked state:" << checked;
}
可切换按钮
// 创建一个可切换按钮
QPushButton *toggleButton = new QPushButton("Toggle Me", this);
toggleButton->setCheckable(true);
// 连接 clicked 信号到槽函数
connect(toggleButton, &QPushButton::clicked, this, &MainWindow::onToggleButtonClicked);
void MainWindow::onToggleButtonClicked(bool checked) {
if (checked) {
qDebug() << "Button is now checked.";
} else {
qDebug() << "Button is now unchecked.";
}
}
pressed信号
当按下鼠标按钮但是还没有释放的时候,立刻发射pressed信号。主要使用场景例如实时的操作,录音或者计时
QPushButton *button = new QPushButton("Press Me", this);
// 连接 pressed 信号到槽函数
connect(button, &QPushButton::pressed, this, &MainWindow::onButtonPressed);
void MainWindow::onButtonPressed() {
qDebug() << "Button pressed.";
}
released()信号
当释放鼠标按钮的时候,立刻发射该信号,可以用于结束pressed信号中开始的操作
QPushButton *button = new QPushButton("Release Me", this);
// 连接 released 信号到槽函数
connect(button, &QPushButton::released, this, &MainWindow::onButtonReleased);
void MainWindow::onButtonReleased() {
qDebug() << "Button released.";
}
toggled(bool checked)
toggled信号就是当按钮的选中状态改变的时候,该信号可以被触发,其中的参数bool checked则是表示按钮的新选中状态
触发场景就是在可切换按钮的时候,当按钮的选中状态发生变化的时候,也就是无论用户点击还是通过代码调用setChecked方法,都会触发该信号,例如可以使用在播放器的开始和停止按钮
// 创建一个可切换按钮
QPushButton *toggleButton = new QPushButton("Toggle Me", this);
toggleButton->setCheckable(true);
// 连接 toggled 信号到槽函数
connect(toggleButton, &QPushButton::toggled, this, &MainWindow::onButtonToggled);
void MainWindow::onButtonToggled(bool checked) {
if (checked) {
qDebug() << "Button is toggled on.";
} else {
qDebug() << "Button is toggled off.";
}
}
信号的区别
- pressed和released:分别是按钮按下和释放的时候触发,不关心动作是否完整
- clicked:则是完成一次完整的点击后触发,也就是按下+释放
- toggled:仅在按钮的选中状态改变的时候触发,适用于可切换按钮
线程安全问题
即使是QT内部的信号与槽机制,也要关注C++中的线程安全机制,在涉及到多线程编程的时候,需要注意线程安全
自定义信号和槽
语法使用
自定义信号
在类中使用signals关键字去定义信号,信号本质上是函数的声明,但是不需要在.cpp文件中实现
注意这个类中必须包含Q_OBJECT宏(用于启动Qt元对象系统),同时信号可以有参数
class MyObject : public QObject {
Q_OBJECT
public:
explicit MyObject(QObject *parent = nullptr);
signals:
void mySignal(); // 无参数的信号
void dataChanged(int newValue); // 带参数的信号
};
自定义槽
在类中使用slots:关键字定义槽函数,槽函数是普通的成员函数,是需要在.cpp文件中实现
槽函数是可以拥有不同的访问权限的,例如public 、protected 、private
class MyObject : public QObject {
Q_OBJECT
public:
explicit MyObject(QObject *parent = nullptr);
public slots:
void mySlot(); // 无参数的槽
void updateData(int value); // 带参数的槽
};
连接信号和槽
通过connect函数实现,参数含义(发送者、发送的信号、接收信号对象的指针、接收者的槽函数) ;注意发送者和接受者是可以不同的对象
connect(sender, &SenderClass::signal, receiver, &ReceiverClass::slot);
MyObject *obj1 = new MyObject();
MyObject *obj2 = new MyObject();
// 连接 obj1 的 mySignal 信号到 obj2 的 mySlot 槽
connect(obj1, &MyObject::mySignal, obj2, &MyObject::mySlot);
发射自定义信号
在类的成员函数中是可以使用emit发射信号的,但是需要注意的是只可以在类的成员函数中发射该类信号,其中emit是其发射信号的关键字
void MyObject::setValue(int value) {
if (m_value != value) {
m_value = value;
emit dataChanged(m_value); // 发射信号
}
}
使用Lambda表达式作为信号槽
可以简化信号槽的书写方法,但是如果信号和槽函数不完全匹配,多余的参数会被忽略
// 信号定义
signals:
void dataChanged(int newValue, const QString &source);
// 槽函数定义
public slots:
void onDataChanged(int newValue);
// 连接
connect(&obj, &MyObject::dataChanged, &obj, &MyObject::onDataChanged);
带参数的信号槽
带参数的信号
- 参数类型可以是任何Qt元对象系统支持的类型,其中可以是基本类型(int , double, bool 等)也可以是Qt类型(QString \ QDateTime等)也可以是自定义类型
signals:
void dataChanged(int newValue);
void textUpdated(const QString &text);
void positionChanged(double x, double y);
带参数的槽
注意其是在头文件中定义,然后在.cpp文件中具体实现
// 具体实现
void Display::updateDisplay(int value) {
lcdNumber->display(value);
}
void Display::setText(const QString &text) {
label->setText(text);
}
参数匹配原则
- 槽函数的参数数量是可以少于信号的参数数量,多余的参数是会忽略的
- 槽函数的参数类型是需要与信号的类型一直或者是可以隐式转换
// 信号有两个参数,槽只有一个参数
signals:
void dataChanged(int newValue, const QString &source);
public slots:
void onDataChanged(int newValue);
// 连接
connect(sender, &Sender::dataChanged, receiver, &Receiver::onDataChanged);
// 信号有两个参数,槽只有一个参数
signals:
void dataChanged(int newValue, const QString &source);
public slots:
void onDataChanged(int newValue);
// 连接
connect(sender, &Sender::dataChanged, receiver, &Receiver::onDataChanged);
信号与槽的连接方式
一对一
- 含义:一个信号连接到一个信号槽,当信号发射的时候,槽函数就被调用一次
- 首先是定义信号,然后实现槽函数,最后将两者连接即可
// 定义信号槽
// 发射信号的类
class Sender : public QObject {
Q_OBJECT
public:
explicit Sender(QObject *parent = nullptr);
signals:
void mySignal(int value);
};
// 接收槽的类
class Receiver : public QObject {
Q_OBJECT
public:
explicit Receiver(QObject *parent = nullptr);
public slots:
void mySlot(int value);
};
// 连接信号槽
Sender *sender = new Sender();
Receiver *receiver = new Receiver();
connect(sender, &Sender::mySignal, receiver, &Receiver::mySlot);
// 发射信号
emit sender->mySignal(42);
// 槽函数的实现
void Receiver::mySlot(int value) {
qDebug() << "Received value:" << value;
}
一对多
- 一个信号连接到多个槽上,也就是当信号发射的时候,所有连接的槽函数都会被调用
- 通过调用多次connect函数,将同一个信号连接到不同的槽
// 定义多个槽
class Receiver1 : public QObject {
Q_OBJECT
public slots:
void slot1(int value) {
qDebug() << "Receiver1 received value:" << value;
}
};
class Receiver2 : public QObject {
Q_OBJECT
public slots:
void slot2(int value) {
qDebug() << "Receiver2 received value:" << value;
}
};
// 连接信号到多个槽
Sender *sender = new Sender();
Receiver1 *receiver1 = new Receiver1();
Receiver2 *receiver2 = new Receiver2();
connect(sender, &Sender::mySignal, receiver1, &Receiver1::slot1);
connect(sender, &Sender::mySignal, receiver2, &Receiver2::slot2);
//类中的函数发送信号实现其功能
emit sender->mySignal(100);
多对一
- 多个信号连接到同一个槽中,当任意一个信号发射的时候,槽函数都会被调用
- 实现的方法同样是通过connnect函数来实现
// 实现多个信号的定义
class Sender1 : public QObject {
Q_OBJECT
public:
explicit Sender1(QObject *parent = nullptr);
signals:
void signal1(int value);
};
class Sender2 : public QObject {
Q_OBJECT
public:
explicit Sender2(QObject *parent = nullptr);
signals:
void signal2(int value);
};
// 定义接收槽的类
class Receiver : public QObject {
Q_OBJECT
public slots:
void commonSlot(int value) {
qDebug() << "Receiver received value:" << value;
}
};
//连接多个信号到同一个槽
Sender1 *sender1 = new Sender1();
Sender2 *sender2 = new Sender2();
Receiver *receiver = new Receiver();
connect(sender1, &Sender1::signal1, receiver, &Receiver::commonSlot);
connect(sender2, &Sender2::signal2, receiver, &Receiver::commonSlot);
// 其他类中发射信号
emit sender1->signal1(10);
emit sender2->signal2(20);
信号槽连接的线程安全
QT中常见的连接类型
- AutoConnection:默认连接类型
- 如果发送者和接收者是在同一个线程,那么就使用DirectConnection(直接调用)
- 如果发送者和接收者在不同线程,那么就使用QueuedConnection(通过事件队列调用)
- DirectConnection
- 直接连接,信号发射时,槽函数在发送者线程被立刻调用
- 类似于直接调用槽函数的行为
- QueuedConnection
- 队列连接,也就是信号发射的时候,槽函数调用被放入接收者线程的事件队列中,槽函数在接收者线程的事件循环中执行
- BolckingQueuedConnection
- 阻塞队列连接,发送者发送完信号后,线程会阻塞,直到槽函数在接收者线程中执行完毕,使适用于需要同步的情况
线程间信号与槽的工作原理
分析事件循环与消息队列
- Qt中的QThread类中是提供了事件循环机制,每个线程可以有自己的事件循环,用于处理事件和消息
- 然后当调用QueuedConnection的时候,信号和槽的调用会被封装成一个事件,放入到接收者线程的事件队列,等待事件循环处理
信号发射与槽的调用流程分析
- 同一线程中发生的连接
- 通过DirectConnection,槽函数在信号发射的时候会立刻调用
- 跨线程的连接
- 使用QueuedConneciton,信号发射的时候,槽函数调用被排入接收者的线程事件队列,一方面是为了打断当前进程正在执行任务,另一方面是为了线程安全
- 使用BlockingQueuedConnecion,发送者线程会阻塞,直到槽函数在接收者线程中执行完毕
QT内部线程安全性实现方法
可重入与线程安全类
- QT的大多数类都是可重入的,也就是说多个线程是可以安全的同时使用同一个类的不同的实例
- 少数类是线程安全的,意味着多个线程是可以同时访问同一个实例的,但是需要内部同步机制,从而保证其线程同步
参数类型的要求
Qt中信号参数是会被复制的,需要确保参数类型是可以复制的,参数类型是需要再QT的元对象系统中注册的
对象与线程绑定
一个QObject实例与创建它的线程相关联,槽函数在其关联的线程中执行,可以使用moveToThread()方法将对象管移动到其他线程
具体实践中保证线程安全的思路
- 尽可能的选用QueueConnection:通过将槽函数放入接收者的队列中,可以避免线程安全问题
- 避免直接访问跨线程的对象:直接访问其他线程中的对象,非常有可能出现数据不一致的问题,所以的为了数据的安全,还是尽量减少访问,非要访问需要加上其安全机制
- 使用线程安全的数据类型:确保参数类型是可复制的和线程安全的,同时避免传递指针或者的引用