1. Qt 三大核心机制
1.1 信号和槽
connect(信号发出者,信号,信号接收者,槽,连接方式(隐藏默认自动连接)) // 五个参数
QObject::connect(const QObject *sender,const char *signal,
const QObject *receiver,const char *method,
Qt::ConnectionType type = Qt::AutoConnection)
1.2 元对象系统
- 元对象系统是一个用于支持 Qt 元编程的框架,允许在运行时动态地查询和操作对象元数据:类名、属性、方法等
- 元对象系统分为三大类
- QObject 类
- Q_OBJECT
- 任何对象要实现信号与槽机制,Q_OBJECT 宏都是强制的
- Qt 的类包含 Q_OBJECT 宏,MOC 编译器会对该类编译成标准的 C++ 代码
- MOC(Meta-Object Compiler,元对象编译器)
- Qt 程序在交由标准编译器编译之前,先要使用 moc 分析 C++ 源文件
- 如果发现在一个头文件中包含了宏 Q_OBJECT,则会生成另外一个 C++ 源文件,这个源文件中包含了 Q_OBJECT 宏的实现代码,这个新的文件名是原文件名前面加上 moc_ 构成,这个新的文件同样将进入编译系统,最终被链接到二进制代码中去。因此,这个新的文件不是 “替换” 掉旧的文件,而是与原文件一起参与编译
- 另外,还可看出:moc 的执行是在预处理器之前,因为预处理器执行之后,Q_OBJECT 宏就不存在了
可以这么理解,moc 把 Qt 中一些不是 C++ 的关键字做了解析,让 C++ 编译器认识,例如:slots,signals,emit 等,moc 会把这些重新编译解析
1.3 事件模型
- 事件的创建
- 鼠标事件,键盘事件,窗口调整事件,模拟事件
- 事件的交付
- Qt 通过调用虚函数 QObject::event() 来交付事件
- 事件循环模型
- 主事件循环通过调用 QCoreApplication::exec() 启动,随着 QCoreApplication::exit() 结束,本地的事件循环可用利用 QEventLoop 构建
- 一般来说,事件是由触发当前的窗口系统产生的,但也可以通过使用 QCoreApplication::sendEvent() 和 QCoreApplication::postEvent() 来手工产生事件:QCoreApplication::sendEvent() 会立即发送事件,QCoreApplication::postEvent() 则会将事件放在事件队列中分发
Qt 中常用的五大模块
- QtCore:提供了 Qt 的核心功能,例如基本的非 GUI 类、线程和事件处理等
- QtGui:提供用户界面(UI)类,例如窗口部件、按钮、标签等。此外,它还包含 QPainter 和 QPalette 等绘图和调色板类
- QtWidgets:是 QtGui 模块的子集,提供了一套完整的可视化 UI 控件库,例如按钮、文本编辑器、表格等,用于构建跨平台的桌面应用程序
- QtNetwork:提供网络编程类,用于创建 TCP 和 UDP 客户端和服务器,以及处理套接字和 HTTP 请求
- QtSql:提供简单易用的数据库访问 API,用于在 Qt 中连接、查询和操作数据源中的数据,包括了对各种数据库的支持,如 SQLite、MySQL、PostgreSQL 等
2. 信号和槽机制
2.1 本质
- Qt 信号和槽是一种用于实现对象间通信的机制,其本质是一种事件驱动的机制,通过信号和槽的连接,当信号被触发时,槽函数会被自动调用,从而实现对象间的通信和交互
- 1. 信号和槽本质是函数
- 信号是一种特殊的函数,用于表示某种事件的发生,当事件发生时,信号会被自动发送出去,通知所有连接到该信号的槽函数
- 槽是一种特殊的函数,用于处理信号的触发事件,当槽函数被连接到某个信号时,当该信号被触发时,槽函数会自动被调用,从而实现对信号的响应
- 2. 信号和槽本质是元对象系统的一部分
- 信号和槽的连接是通过 Qt 的元对象系统实现的,每个 QObject 派生类都有一个元对象,用于存储该类的属性、方法和信号槽信息,通过元对象系统,可以在运行时动态地连接信号和槽,从而实现对象间的通信
- 1. 信号和槽本质是函数
2.2 优缺点
-
优点
- 1. 类型安全:需要关联的信号槽的签名必须是等同的,即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同,若信号和槽签名不一致,编译器会报错
- 2. 松散耦合:信号和槽机制减弱了 Qt 对象的耦合度。激发信号的 Qt 对象无需知道是哪个对象的哪个信号槽接收它发出的信号,它只需在适当的时间发送适当的信号即可,而不需要关心是否被接受和哪个对象接受了
- 3. 跨线程:信号和槽机制支持跨线程的事件处理,可以将信号和槽连接在不同的线程中,这样可以实现不同线程之间的通信
- 4. 灵活性高:一个信号可以关联多个槽,或多个信号关联同一个槽
Qt 自定义一个信号槽并触发这个信号,Qt 多个信号如何关联一并处理?
- 方法一:在发送信号时,也发送一个 int 类型数字(标志),这样在槽函数触发时可知道是哪个信号发出
- 方法二:在槽函数内有获取发送信号的函数,通过 sender() 函数获取发送信号
-
缺点
- 速度较慢:与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢 10 倍
- 原因:① 需要定位接收信号的对象;② 安全地遍历所有关联槽;③ 多线程时,信号需要排队等待
然而,与创建对象的 new 操作及删除对象的 delete 操作相比,信号和槽的运行代价只是他们很少的一部分,信号和槽机制导致的这点性能损耗,对实时应用程序是可以忽略的
- 不安全:由于信号和槽机制可以动态连接和断开信号和槽,因此在使用时需要注意安全性问题,避免出现槽函数被误调用的情况
- 速度较慢:与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢 10 倍
2.3 信号和槽的四种写法
-
1. 宏(Qt 4 写法)
- 不推荐这种写法,如果 SIGNAL写错了或者信号名字、槽函数名字写错了,编译器检查不出来,导致程序无响应,引起不必要的误解
connect(this,SIGNAL(clicked()),this,SLOT(colse())); // 连接方式(隐藏默认自动连接))
-
2. 函数指针(Qt 5 写法,推荐)
connect(this,&mainwindow::my_signal,this,&mainwindow::my_slot);
-
3. 重载函数指针 QOverload
connect(this,QOverload<参数>::of(&mainwindow::my_signal),this,QOverload<参数>::of(&mainwindow::my_slot));
-
4. lambda 表达式(匿名函数代替槽)
- 适用于槽函数代码比较少的逻辑
connect(this,&mainwindow::my_signal,this,[=]{ qDebug() << 100; });
-
QDialog 对话框类的几个公有槽函数
- accept(),功能是关闭对话框,表示肯定的选择,如对话框上的 “确定” 按钮
- reject(),功能是关闭对话框,表示否定的选择,如对话框上的 “取消” 按钮
- close(),功能是关闭对话框
-
通过 Go to slot 自动生成的槽函数命名
void on_xxx_clicked();
槽函数的参数可以少于信号的参数
- 槽函数本身参数比信号的少
- 槽函数参数带有默认参数(除去默认参数外,槽函数的参数必须小于等于信号的参数)
3. 多线程的信号和槽(同步/异步)控制
- 可通过 connect 的第五个参数 Qt::connectType 控制信号槽执行时所在的线程,有以下五种连接方式
- 自动连接 Qt::AutoConnection(默认值)
- 单线程时为直接连接函数,多线程时为队列连接函数
- 直接连接 Qt::DirectConnection
- 槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程
- 效果看上去就像是直接在信号发送位置调用了槽函数
- 队列连接 Qt::QueuedConnection
- 槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程
- 发送信号后槽函数不会立刻被调用,等接收者当前函数执行完,进入事件循环后,槽函数才会被调用
- 阻塞队列连接 Qt::BlockingQueuedConnection
- 槽函数的调用时机与队列连接一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完
- 接收者和发送者绝对不能在一个线程,否则程序会死锁
- 单一连接 Qt::UniqueConnection
- 这个 flag 可以通过按位或(|)与以上四个结合在一起使用
- 当这个 flag 设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败(避免重复连接)
QObject::connect(const QObject *sender,const char *signal, const QObject *receiver,const char *method, Qt::ConnectionType type = Qt::AutoConnection)
- 自动连接 Qt::AutoConnection(默认值)
4. Qt 信号和槽的调用流程
- 1、MOC 查找头文件中的 signal 与 slots,标记出信号槽。将信号槽信息储存到类静态变量 staticMetaObject 中,并按照声明的顺序进行存放,建立索引
- 2、connect 链接,将信号槽的索引信息放到一个双向链表中,彼此配对
- 3、emit 被调用,调用信号函数,且传递发送信号的对象指针,元对象指针,信号索引,参数列表到 active 函数
- 4、active 函数在双向链表中找到所有与信号对应的槽索引,根据槽索引找到槽函数,执行槽函数
5. Qt5 相比 Qt4 的信号和槽改进
- 编译期:检查信号与槽是否存在,参数类型检查 Q_OBJECT 是否存在
- 信号可以和普通的函数、类的普通成员函数、lambda 函数连接(不再局限于信号函数和槽函数)
- 参数可以是 typedef 的或使用不同的 namespace specifier
- 可以允许一些自动的类型转换(即信号和槽参数类型不必完全匹配)
6. Qt 信号重载了,如何确定连接哪个信号?
- 可以通过使用 SIGNAL() 宏来明确定义要连接的特定信号,SIGNAL() 宏接受函数签名作为参数,因此可以准确地指定要连接的重载信号
connect(sender,SIGNAL(signalOverloaded(int)),receiver,SLOT(yourSlot())); connect(sender,SIGNAL(signalOverloaded(double)),receiver,SLOT(yourSlot()));
函数签名:通常指的是函数的名称以及它的参数列表
7. QObject 类
7.1 QObject 类特性
- QObject 类是 Qt 所有类的基类
- QObject 是 Qt 对象模型的核心
- 这个模型的中心要素就是信号和槽对象沟通机制
- 对象树都是通过 QObject 组织起来的
- 当以一个对象作为父类创建一个新的对象时,这个新对象会被自动加入到父类的 children() 队列中,这个父类有子类的所有权,能在父类的析构函数中自动删除子类,可通过 findChild() 和 findChildren() 函数来寻找子类
- 每个对象都一个对象名称 objectName()
- 对象可以通过 event() 函数来接收事情以及过滤来自其他对象的事件
- QObject 还提供了基本的时间支持,QTimer 类提高了更高层次的时间支持
- 任何对象要实现信号与槽机制,Q_OBJECT 宏都是强制的,不管是否真正用到信号与槽机制,最好在所有 QObject 子类使用 Q_OBJECT 宏,以避免出现一些不必要的错误
7.2 QObject 是否是线程安全的?
- QObject 及其所有子类都不是线程安全的(但都是可重入的),因此不能有两个线程同时访问一个 QObject 对象,除非这个对象的内部数据都已经很好地序列化(例如为每个数据访问加锁)
7.3 如何安全的在另一个线程中调用 QObject 对象的接口?
- 方法一:使用 QMetaObject::invokeMethod() 静态函数在目标 QObject 对象所在的线程中调用其方法,这个方法可以确保在目标对象的线程上下文中执行所需要执行的操作
- QMetaObject::invokeMethod() 是一个 Qt 框架中的静态函数,用于在运行时调用 QObject 对象的方法。通过这个函数,可以在任意线程中异步地调用指定 QObject 对象的成员函数,而无需直接触及到对象以及对象所在线程的细节,使得跨线程调用更为方便和安全
// 在发送者线程中调用 // objectOnOtherThread:另一个线程中的 QObject 对象 // "slotName":要调用的槽函数名称 // Qt::QueuedConnection 确保将该调用放入目标线程的事件队列中异步执行,确保线程安全 QMetaObject::invokeMethod(objectOnOtherThread,"slotName",Qt::QueuedConnection,Q_ARG(int,arg1),Q_ARG(QString,arg2));
- 方法二:使用信号与槽机制来在线程间进行通信。当连接传递的信号和槽时,可以使用 Qt::QueuedConnection 以确保槽函数在目标线程中执行,而不是直接在信号发出的线程中执行
7.4 QObject 的线程依附性是否可以改变?
- 调用 QObject::moveToThread() 函数,该函数会改变一个对象及其所有子对象的线程依附性
- 由于 QObject 本身是线程不安全的,因此 moveToThread 接口的调用必须在 QObject 对象所在的线程内调用
8. Qt 对象树机制
- QT 提供对象树机制,能够自动、有效的组织和管理继承自 QObject 的对象
- 每个继承自 QObject 类的对象通过它的对象链表(QObjectList)来管理子类对象,当用户创建一个子对象时,其对象链表相应更新子类对象的信息,对象链表可通过 children() 获取
- 当父类对象析构时,其对象链表中的所有(子类)对象也会被析构,父对象会自动将其从父对象列表中删除,QT 保证没有对象会被 delete 两次
- 开发中手动回收资源时建议使用 deleteLater 代替 delete,因为 deleteLater 多次是安全的
9. Qt 3 个窗口类的区别
- QMainWindow
- 包含菜单栏、工具栏、状态栏
- QMainWindow 使用的场景不多
- QWidget
- 一个普通的窗口,不包含菜单栏、状态栏,除了登录界面
- 新建项目时建议使用 Qwidget,因为大部分的窗口可能都要做成无边框窗口,需要自定义标题栏,实现拉伸等
QFrame 与 QWidget 的区别
- 1. 继承关系:QFrame 继承自 QWidget,所以 QFrame 具有 QWidget 的所有功能
- 2. 功能:QFrame 提供了一个简单的框架,可以作为其他控件的容器,它还可以用来绘制简单的图形如线条,QWidget 没有这样的功能,但是提供了基础的 GUI 组件功能,如设置尺寸和位置等
- 3. 外观:QFrame 可以有边框和背景颜色,因此外观更加丰富,而 QWidget 只有背景颜色,没有边框
- 通常,当需要一个简单的框架时,使用 QFrame,当需要基础的 GUI 组件功能时,使用 QWidget
- QDialog
- 对话框,常用来做登录窗口、弹出窗口 (例如设置界面)
10. Qt 内存管理机制
- 手动内存管理
- Qt 中可以像在普通的 C++ 中一样使用 new 和 delete 来手动管理内存。但是需要注意的是,如果不小心造成内存泄漏,Qt 的一些机制(比如信号和槽)可能会导致难以察觉和调试的问题
- 父子关系
- Qt 的对象间可以建立父子关系,父对象在销毁时会自动销毁其所有子对象,这种机制可以简化内存管理,因为只需要关心顶层父对象的生命周期
- 智能指针
- Qt 提供了一些智能指针类,比如 QSharedPointer、QWeakPointer 和 QScopedPointer,它们能够帮助你更安全地管理内存。使用这些类可以减少内存泄漏和悬挂指针的风险
- 内存自动清理
- Qt 提供了一些特殊的类,比如 QPointer,可以在对象销毁后自动将指针设置为 nullptr,避免悬挂指针问题