• 文件浏览器
  • Application Boost详解 C++标准库 C++编译器 C++语言基础 C++软件开发 C++进阶知识 CMake Folly库 000 《C++知识框架》 001 《C++ 深度解析:从入门到精通 (C++ Deep Dive: From Beginner to Expert)》 002 《C++ 基础 (Fundamentals) - 全面且深度解析》 003 《C++编程语言:全面深度解析 (C++ Programming Language: Comprehensive Deep Dive)》 004 《C++ 面向对象编程 (Object-Oriented Programming - OOP) 深度解析》 005 《C++ 标准模板库 (STL) 深度解析与应用 (C++ Standard Template Library (STL): In-depth Analysis and Application)》 006 《现代 C++ (Modern C++) 全面且深度解析》 007 《C++ 现代编程:C++11/14/17/20 新特性深度解析 (Modern C++ Programming: In-depth Analysis of C++11/14/17/20 New Features)》 008 《C++ 模板元编程 (Template Metaprogramming - TMP) 深度解析》 009 《C++ 并发与多线程深度解析 (C++ Concurrency and Multithreading: In-depth Analysis)》 010 《C++ 异常处理 (Exception Handling) 深度解析》 011 《C++ 泛型编程:原理、实践与高级应用 (C++ Generic Programming: Principles, Practice, and Advanced Applications)》 012 《C++ 元编程 (Metaprogramming) 深度解析:从入门到精通》 013 《C++ 网络编程深度解析 (In-depth Analysis of C++ Network Programming)》 014 《C++ 系统编程深度解析 (C++ System Programming Deep Dive)》 015 《C++ 嵌入式系统开发 (Embedded Systems Development) 深度解析》 016 《C++ 性能优化 (Performance Optimization): 深度解析与实战指南》 017 《C++ 测试 (Testing) 深度解析》 018 《C++ 构建系统和工具深度解析 (C++ Build Systems and Tools: In-depth Analysis)》 019 《C++ GUI 编程:Qt 框架深度解析》 020 《C++ 游戏开发 (Game Development) 深度解析》 021 《C++ 数据库编程 (Database Programming): 全面解析与实践指南》 022 《C++ 科学计算和高性能计算》 023 《C++ 元数据与反射:深度解析与实践 (C++ Metadata and Reflection: In-depth Analysis and Practice)》 024 《C++ 跨语言互操作性深度解析 (Deep Dive into C++ Interoperability)》 025 《C++ 标准库深入 (In-depth Standard Library)》 026 《CMake 详解:构建跨平台项目的实践指南 (CMake Explained: A Practical Guide to Cross-Platform Project Building)》 027 《Boost 库完全解析 (Boost Library Complete and In-depth Analysis)》 028 《深入探索 Folly 库:原理、实践与高级应用》 029 《OpenSSL C++ 开发:深度解析与实战》 030 《Crypto++的C++开发:全面深度解析与实践指南 (Crypto++ C++ Development: Comprehensive Deep Dive and Practical Guide)》 031 《mbedtls的C++开发:全面与深度解析》

    019 《C++ GUI 编程:Qt 框架深度解析》


    作者Lou Xiao, gemini创建时间2025-04-22 19:41:47更新时间2025-04-22 19:41:47

    🌟🌟🌟本文由Gemini 2.0 Flash Thinking Experimental 01-21生成,用来辅助学习。🌟🌟🌟

    书籍大纲

    ▮▮ 1. 起步:C++ GUI 编程概览
    ▮▮▮▮ 1.1 1.1 什么是 GUI 编程?
    ▮▮▮▮▮▮ 1.1.1 1.1.1 GUI 的定义与优势
    ▮▮▮▮▮▮ 1.1.2 1.1.2 GUI 编程的应用领域
    ▮▮▮▮▮▮ 1.1.3 1.1.3 GUI 编程的核心概念:事件驱动
    ▮▮▮▮ 1.2 1.2 C++ 与 GUI 开发
    ▮▮▮▮▮▮ 1.2.1 1.2.1 C++ 的优势:性能与控制力
    ▮▮▮▮▮▮ 1.2.2 1.2.2 C++ GUI 框架的选择
    ▮▮▮▮ 1.3 1.3 为什么选择 Qt 框架?
    ▮▮▮▮▮▮ 1.3.1 1.3.1 Qt 的特性与优势:跨平台、模块化、易用性
    ▮▮▮▮▮▮ 1.3.2 1.3.2 Qt 的工具链:Qt Creator, qmake, CMake
    ▮▮▮▮▮▮ 1.3.3 1.3.3 Qt 的社区与资源
    ▮▮▮▮ 1.4 1.4 搭建 Qt 开发环境
    ▮▮▮▮▮▮ 1.4.1 1.4.1 Windows 环境下的 Qt 安装与配置
    ▮▮▮▮▮▮ 1.4.2 1.4.2 macOS 环境下的 Qt 安装与配置
    ▮▮▮▮▮▮ 1.4.3 1.4.3 Linux 环境下的 Qt 安装与配置
    ▮▮ 2. Qt 基础:核心概念与架构
    ▮▮▮▮ 2.1 2.1 Qt 元对象系统 (Meta-Object System)
    ▮▮▮▮▮▮ 2.1.1 2.1.1 元对象系统的组成部分:QObject, MOC (元对象编译器), 反射 (Reflection)
    ▮▮▮▮▮▮ 2.1.2 2.1.2 元对象系统的作用:信号与槽、属性系统、动态属性
    ▮▮▮▮ 2.2 2.2 信号与槽 (Signals and Slots) 机制
    ▮▮▮▮▮▮ 2.2.1 2.2.1 信号与槽的基本概念:信号的发射与槽的连接
    ▮▮▮▮▮▮ 2.2.2 2.2.2 信号与槽的优势:类型安全、松耦合
    ▮▮▮▮▮▮ 2.2.3 2.2.3 信号与槽的连接方式:直接连接、队列连接、自动连接
    ▮▮▮▮▮▮ 2.2.4 2.2.4 自定义信号与槽
    ▮▮▮▮ 2.3 2.3 Qt 对象模型 (Object Model)
    ▮▮▮▮▮▮ 2.3.1 2.3.1 对象树:父子关系与对象生命周期管理
    ▮▮▮▮▮▮ 2.3.2 2.3.2 Qt 的内存管理:自动内存回收机制
    ▮▮▮▮ 2.4 2.4 Qt 的模块化设计
    ▮▮▮▮▮▮ 2.4.1 2.4.1 常用 Qt 模块介绍:QtCore, QtGui, QtWidgets, QtNetwork, QtSql 等
    ▮▮▮▮▮▮ 2.4.2 2.4.2 模块的依赖关系与选择
    ▮▮ 3. 窗口与控件:构建用户界面
    ▮▮▮▮ 3.1 3.1 窗口 (Window) 基础
    ▮▮▮▮▮▮ 3.1.1 3.1.1 顶层窗口 (Top-Level Windows) 与子窗口 (Child Windows)
    ▮▮▮▮▮▮ 3.1.2 3.1.2 QWidget 类:所有控件的基类
    ▮▮▮▮▮▮ 3.1.3 3.1.3 创建和显示窗口:QMainWindow, QDialog, QWidget
    ▮▮▮▮▮▮ 3.1.4 3.1.4 窗口的属性设置:标题、大小、位置、图标
    ▮▮▮▮ 3.2 3.2 常用控件 (Widgets) 详解
    ▮▮▮▮▮▮ 3.2.1 3.2.1 按钮 (QPushButton):事件响应与交互
    ▮▮▮▮▮▮ 3.2.2 3.2.2 标签 (QLabel):显示文本与图像
    ▮▮▮▮▮▮ 3.2.3 3.2.3 文本框 (QLineEdit):文本输入
    ▮▮▮▮▮▮ 3.2.4 3.2.4 文本编辑框 (QTextEdit):多行文本编辑
    ▮▮▮▮▮▮ 3.2.5 3.2.5 复选框 (QCheckBox) 与单选按钮 (QRadioButton):选项选择
    ▮▮▮▮▮▮ 3.2.6 3.2.6 下拉框 (QComboBox) 与列表框 (QListWidget):列表选择
    ▮▮▮▮▮▮ 3.2.7 3.2.7 滑动条 (QSlider) 与微调框 (QSpinBox):数值调节
    ▮▮▮▮▮▮ 3.2.8 3.2.8 进度条 (QProgressBar) 与刻度盘 (QDial):状态显示
    ▮▮▮▮ 3.3 3.3 控件的属性与样式
    ▮▮▮▮▮▮ 3.3.1 3.3.1 控件的常用属性:文本、字体、颜色、大小
    ▮▮▮▮▮▮ 3.3.2 3.3.2 Qt 样式表 (Style Sheets):CSS 风格的界面美化
    ▮▮ 4. 布局管理:灵活的界面设计
    ▮▮▮▮ 4.1 4.1 布局管理器 (Layout Managers) 概述
    ▮▮▮▮▮▮ 4.1.1 4.1.1 为什么需要布局管理器?
    ▮▮▮▮▮▮ 4.1.2 4.1.2 Qt 的布局管理器类型:QHBoxLayout, QVBoxLayout, QGridLayout, QFormLayout, QStackedLayout
    ▮▮▮▮ 4.2 4.2 常用布局管理器详解与应用
    ▮▮▮▮▮▮ 4.2.1 4.2.1 QHBoxLayout 与 QVBoxLayout:水平与垂直布局
    ▮▮▮▮▮▮ 4.2.2 4.2.2 QGridLayout:网格布局的精细控制
    ▮▮▮▮▮▮ 4.2.3 4.2.3 QFormLayout:表单布局的快速创建
    ▮▮▮▮▮▮ 4.2.4 4.2.4 QStackedLayout:堆叠布局与页面切换
    ▮▮▮▮▮▮ 4.2.5 4.2.5 布局的嵌套与组合
    ▮▮▮▮ 4.3 4.3 尺寸策略 (Size Policies) 与布局微调
    ▮▮▮▮▮▮ 4.3.1 4.3.1 尺寸策略 (QSizePolicy) 的作用与属性
    ▮▮▮▮▮▮ 4.3.2 4.3.2 最小尺寸 (minimumSize)、最大尺寸 (maximumSize) 与尺寸提示 (sizeHint)
    ▮▮▮▮▮▮ 4.3.3 4.3.3 布局的间距 (spacing) 与边距 (margin) 设置
    ▮▮ 5. 事件处理:响应用户交互
    ▮▮▮▮ 5.1 5.1 Qt 事件处理机制概述
    ▮▮▮▮▮▮ 5.1.1 5.1.1 事件 (Event) 的类型与分类
    ▮▮▮▮▮▮ 5.1.2 5.1.2 事件循环 (Event Loop) 的工作原理
    ▮▮▮▮▮▮ 5.1.3 5.1.3 事件传播 (Event Propagation):事件的传递路径
    ▮▮▮▮ 5.2 5.2 常用事件处理方法
    ▮▮▮▮▮▮ 5.2.1 5.2.1 重写事件处理函数 (Event Handlers):paintEvent(), mousePressEvent(), keyPressEvent() 等
    ▮▮▮▮▮▮ 5.2.2 5.2.2 使用信号与槽连接事件:QPushButton::clicked(), QLineEdit::textChanged() 等
    ▮▮▮▮▮▮ 5.2.3 5.2.3 事件过滤器 (Event Filter):全局事件监听
    ▮▮▮▮ 5.3 5.3 鼠标事件处理
    ▮▮▮▮▮▮ 5.3.1 5.3.1 鼠标点击事件:mousePressEvent(), mouseReleaseEvent(), mouseDoubleClickEvent()
    ▮▮▮▮▮▮ 5.3.2 5.3.2 鼠标移动事件:mouseMoveEvent(), 鼠标跟踪 (Mouse Tracking)
    ▮▮▮▮▮▮ 5.3.3 5.3.3 鼠标滚轮事件:wheelEvent()
    ▮▮▮▮ 5.4 5.4 键盘事件处理
    ▮▮▮▮▮▮ 5.4.1 5.4.1 按键按下事件:keyPressEvent(), 按键修饰符 (Modifiers)
    ▮▮▮▮▮▮ 5.4.2 5.4.2 按键释放事件:keyReleaseEvent()
    ▮▮▮▮▮▮ 5.4.3 5.4.3 键盘焦点 (Keyboard Focus) 管理
    ▮▮ 6. 图形与绘制:自定义界面元素
    ▮▮▮▮ 6.1 6.1 Qt 绘图系统 (Painting System) 概述
    ▮▮▮▮▮▮ 6.1.1 6.1.1 绘图设备 (Paint Device):QWidget, QImage, QPixmap, QPicture
    ▮▮▮▮▮▮ 6.1.2 6.1.2 画家 (QPainter):绘图操作的核心类
    ▮▮▮▮▮▮ 6.1.3 6.1.3 画笔 (QPen)、画刷 (QBrush)、字体 (QFont):绘图属性设置
    ▮▮▮▮▮▮ 6.1.4 6.1.4 坐标系统 (Coordinate System) 与变换 (Transformation)
    ▮▮▮▮ 6.2 6.2 常用 2D 图形绘制
    ▮▮▮▮▮▮ 6.2.1 6.2.1 绘制基本图形:drawLine(), drawRect(), drawEllipse(), drawPolygon()
    ▮▮▮▮▮▮ 6.2.2 6.2.2 绘制文本:drawText(),字体设置与排版
    ▮▮▮▮▮▮ 6.2.3 6.2.3 绘制路径 (QPainterPath):复杂图形的构建
    ▮▮▮▮▮▮ 6.2.4 6.2.4 填充与裁剪 (Fill and Clip)
    ▮▮▮▮ 6.3 6.3 图像处理与显示
    ▮▮▮▮▮▮ 6.3.1 6.3.1 加载图像:QImage, QPixmap
    ▮▮▮▮▮▮ 6.3.2 6.3.2 显示图像:QLabel, QGraphicsView
    ▮▮▮▮▮▮ 6.3.3 6.3.3 图像的基本操作:缩放、旋转、裁剪
    ▮▮▮▮ 6.4 6.4 自定义控件 (Custom Widgets) 的绘制
    ▮▮▮▮▮▮ 6.4.1 6.4.1 继承 QWidget 创建自定义控件
    ▮▮▮▮▮▮ 6.4.2 6.4.2 在 paintEvent() 中进行自定义绘制
    ▮▮▮▮▮▮ 6.4.3 6.4.3 响应用户交互的自定义控件
    ▮▮ 7. 模型/视图架构:数据与界面的分离
    ▮▮▮▮ 7.1 7.1 模型/视图架构概述
    ▮▮▮▮▮▮ 7.1.1 7.1.1 模型 (Model)、视图 (View)、委托 (Delegate) 的角色与关系
    ▮▮▮▮▮▮ 7.1.2 7.1.2 模型/视图架构的优势:数据与界面分离、代码复用
    ▮▮▮▮ 7.2 7.2 模型 (Model) 的实现
    ▮▮▮▮▮▮ 7.2.1 7.2.1 Qt 提供的模型类:QStringListModel, QStandardItemModel, QFileSystemModel, QSqlQueryModel
    ▮▮▮▮▮▮ 7.2.2 7.2.2 创建自定义模型:继承 QAbstractListModel, QAbstractTableModel, QAbstractItemModel
    ▮▮▮▮▮▮ 7.2.3 7.2.3 模型的接口:data(), rowCount(), columnCount(), headerData()
    ▮▮▮▮ 7.3 7.3 视图 (View) 的使用
    ▮▮▮▮▮▮ 7.3.1 7.3.1 Qt 提供的视图类:QListView, QTableView, QTreeView
    ▮▮▮▮▮▮ 7.3.2 7.3.2 视图与模型的连接:setModel()
    ▮▮▮▮▮▮ 7.3.3 7.3.3 视图的配置:显示属性、选择模式、排序、过滤
    ▮▮▮▮ 7.4 7.4 委托 (Delegate) 的定制
    ▮▮▮▮▮▮ 7.4.1 7.4.1 委托的作用:控制数据的显示与编辑
    ▮▮▮▮▮▮ 7.4.2 7.4.2 默认委托与自定义委托
    ▮▮▮▮▮▮ 7.4.3 7.4.3 创建自定义委托:继承 QStyledItemDelegate, QItemDelegate
    ▮▮▮▮▮▮ 7.4.4 7.4.4 委托的接口:paint(), createEditor(), setEditorData(), editorDataToModelData()
    ▮▮ 8. Qt 多线程:提升程序性能
    ▮▮▮▮ 8.1 8.1 多线程编程基础
    ▮▮▮▮▮▮ 8.1.1 8.1.1 进程 (Process) 与线程 (Thread) 的概念
    ▮▮▮▮▮▮ 8.1.2 8.1.2 多线程的优势:提高响应性、并发性、资源利用率
    ▮▮▮▮▮▮ 8.1.3 8.1.3 多线程编程的挑战:线程同步、数据竞争、死锁
    ▮▮▮▮ 8.2 8.2 Qt 的线程类:QThread
    ▮▮▮▮▮▮ 8.2.1 8.2.1 创建线程:继承 QThread, 重写 run() 函数
    ▮▮▮▮▮▮ 8.2.2 8.2.2 启动线程:start(), 线程的生命周期管理
    ▮▮▮▮▮▮ 8.2.3 8.2.3 线程间的通信:信号与槽、事件队列
    ▮▮▮▮ 8.3 8.3 Qt 并发框架 (Concurrency Framework):QtConcurrent
    ▮▮▮▮▮▮ 8.3.1 8.3.1 QtConcurrent 概述:函数式编程风格的并发
    ▮▮▮▮▮▮ 8.3.2 8.3.2 QtConcurrent::run():在线程池中运行函数
    ▮▮▮▮▮▮ 8.3.3 8.3.3 QtConcurrent::map(), QtConcurrent::filter(), QtConcurrent::reduce():并行算法
    ▮▮▮▮ 8.4 8.4 线程同步与数据保护
    ▮▮▮▮▮▮ 8.4.1 8.4.1 互斥锁 (Mutex):QMutex, QMutexLocker
    ▮▮▮▮▮▮ 8.4.2 8.4.2 读写锁 (Read-Write Lock):QReadWriteLock, QReadLocker, QWriteLocker
    ▮▮▮▮▮▮ 8.4.3 8.4.3 条件变量 (Condition Variable):QWaitCondition
    ▮▮▮▮▮▮ 8.4.4 8.4.4 原子操作 (Atomic Operations):QAtomicInt, QAtomicPointer
    ▮▮ 9. 网络编程:Qt 网络模块应用
    ▮▮▮▮ 9.1 9.1 Qt 网络模块 (Qt Network Module) 概述
    ▮▮▮▮▮▮ 9.1.1 9.1.1 Qt 网络模块的功能:TCP, UDP, HTTP, SSL/TLS
    ▮▮▮▮▮▮ 9.1.2 9.1.2 常用网络类:QTcpSocket, QUdpSocket, QTcpServer, QNetworkAccessManager
    ▮▮▮▮ 9.2 9.2 TCP 编程:客户端与服务器
    ▮▮▮▮▮▮ 9.2.1 9.2.1 TCP 客户端 (Client) 的创建:QTcpSocket
    ▮▮▮▮▮▮ 9.2.2 9.2.2 TCP 服务器 (Server) 的创建:QTcpServer, QTcpSocket
    ▮▮▮▮▮▮ 9.2.3 9.2.3 TCP 数据传输:发送与接收数据
    ▮▮▮▮▮▮ 9.2.4 9.2.4 异步 TCP 通信:信号与槽
    ▮▮▮▮ 9.3 9.3 UDP 编程:数据报通信
    ▮▮▮▮▮▮ 9.3.1 9.3.1 UDP 套接字 (Socket) 的创建:QUdpSocket
    ▮▮▮▮▮▮ 9.3.2 9.3.2 UDP 数据传输:发送与接收数据报
    ▮▮▮▮▮▮ 9.3.3 9.3.3 UDP 广播 (Broadcast) 与组播 (Multicast)
    ▮▮▮▮ 9.4 9.4 HTTP 编程:QNetworkAccessManager
    ▮▮▮▮▮▮ 9.4.1 9.4.1 QNetworkAccessManager 的使用:发送 HTTP 请求
    ▮▮▮▮▮▮ 9.4.2 9.4.2 HTTP 请求的配置:请求头 (Headers), 请求体 (Body)
    ▮▮▮▮▮▮ 9.4.3 9.4.3 HTTP 响应的处理:状态码 (Status Code), 响应头 (Headers), 响应体 (Body)
    ▮▮▮▮▮▮ 9.4.4 9.4.4 处理 JSON 数据
    ▮▮ 10. 数据存储与数据库:Qt SQL 模块应用
    ▮▮▮▮ 10.1 10.1 Qt SQL 模块 (Qt SQL Module) 概述
    ▮▮▮▮▮▮ 10.1.1 10.1.1 Qt SQL 模块的功能:数据库连接、SQL 查询、事务处理
    ▮▮▮▮▮▮ 10.1.2 10.1.2 支持的数据库类型:SQLite, MySQL, PostgreSQL, ODBC, TDS
    ▮▮▮▮▮▮ 10.1.3 10.1.3 常用 SQL 类:QSqlDatabase, QSqlQuery, QSqlTableModel, QSqlRelationalTableModel
    ▮▮▮▮ 10.2 10.2 数据库连接与配置
    ▮▮▮▮▮▮ 10.2.1 10.2.1 QSqlDatabase 的使用:添加数据库连接
    ▮▮▮▮▮▮ 10.2.2 10.2.2 数据库驱动 (Driver) 的选择与加载
    ▮▮▮▮▮▮ 10.2.3 10.2.3 连接参数的配置:主机名、端口号、用户名、密码、数据库名
    ▮▮▮▮▮▮ 10.2.4 10.2.4 错误处理与连接状态检查
    ▮▮▮▮ 10.3 10.3 SQL 查询与数据操作
    ▮▮▮▮▮▮ 10.3.1 10.3.1 QSqlQuery 的使用:执行 SQL 语句
    ▮▮▮▮▮▮ 10.3.2 10.3.2 预处理语句 (Prepared Statements):提高性能与安全性
    ▮▮▮▮▮▮ 10.3.3 10.3.3 获取查询结果:next(), value()
    ▮▮▮▮▮▮ 10.3.4 10.3.4 事务 (Transaction) 处理:begin(), commit(), rollback()
    ▮▮▮▮ 10.4 10.4 模型与数据库的集成
    ▮▮▮▮▮▮ 10.4.1 10.4.1 QSqlTableModel 的使用:表格数据的模型
    ▮▮▮▮▮▮ 10.4.2 10.4.2 QSqlRelationalTableModel 的使用:关联表格的模型
    ▮▮▮▮▮▮ 10.4.3 10.4.3 数据的提交与回滚:submitAll(), revertAll()
    ▮▮ 11. Qt Quick 与 QML:现代 UI 开发
    ▮▮▮▮ 11.1 11.1 Qt Quick 与 QML 概述
    ▮▮▮▮▮▮ 11.1.1 11.1.1 Qt Quick 的特点:声明式 UI, 动画效果, 硬件加速
    ▮▮▮▮▮▮ 11.1.2 11.1.2 QML 语言:声明式语法, JavaScript 集成
    ▮▮▮▮▮▮ 11.1.3 11.1.3 Qt Quick 应用的架构:QML 前端与 C++ 后端
    ▮▮▮▮ 11.2 11.2 QML 基础语法
    ▮▮▮▮▮▮ 11.2.1 11.2.1 QML 元素 (Element):Rectangle, Text, Image, MouseArea
    ▮▮▮▮▮▮ 11.2.2 11.2.2 属性 (Property) 与属性绑定 (Property Binding)
    ▮▮▮▮▮▮ 11.2.3 11.2.3 信号 (Signal) 与槽 (Slot):QML 中的事件处理
    ▮▮▮▮▮▮ 11.2.4 11.2.4 JavaScript 集成:在 QML 中编写 JavaScript 代码
    ▮▮▮▮ 11.3 11.3 Qt Quick 布局与定位
    ▮▮▮▮▮▮ 11.3.1 11.3.1 定位器 (Positioners):Row, Column, Grid, Flow
    ▮▮▮▮▮▮ 11.3.2 11.3.2 锚点 (Anchors):相对定位
    ▮▮▮▮▮▮ 11.3.3 11.3.3 布局项 (Layout Items):Layout 属性
    ▮▮▮▮ 11.4 11.4 Qt Quick 动画与特效
    ▮▮▮▮▮▮ 11.4.1 11.4.1 属性动画 (Property Animation):NumberAnimation, ColorAnimation, RotationAnimation
    ▮▮▮▮▮▮ 11.4.2 11.4.2 过渡效果 (Transitions):Animated Property Changes
    ▮▮▮▮▮▮ 11.4.3 11.4.3 状态 (States) 与状态组 (State Groups):复杂动画的组织与管理
    ▮▮▮▮▮▮ 11.4.4 11.4.4 粒子效果 (Particle Effects):创建炫酷的视觉效果
    ▮▮▮▮ 11.5 11.5 C++ 与 QML 交互
    ▮▮▮▮▮▮ 11.5.1 11.5.1 将 C++ 数据暴露给 QML:属性、信号、槽
    ▮▮▮▮▮▮ 11.5.2 11.5.2 在 C++ 中调用 QML 函数与访问 QML 对象
    ▮▮▮▮▮▮ 11.5.3 11.5.3 使用 Context Properties 和 Context Objects 传递数据
    ▮▮ 12. 测试与调试:保障程序质量
    ▮▮▮▮ 12.1 12.1 单元测试 (Unit Testing) 基础
    ▮▮▮▮▮▮ 12.1.1 12.1.1 什么是单元测试?
    ▮▮▮▮▮▮ 12.1.2 12.1.2 单元测试的优势:尽早发现 Bug, 代码重构保障
    ▮▮▮▮▮▮ 12.1.3 12.1.3 单元测试框架:Qt Test
    ▮▮▮▮ 12.2 12.2 Qt Test 框架使用
    ▮▮▮▮▮▮ 12.2.1 12.2.1 创建测试类:继承 QObject, 添加 Q_OBJECT 宏
    ▮▮▮▮▮▮ 12.2.2 12.2.2 编写测试函数:使用 QTest::addColumn, QCOMPARE, QVERIFY 等宏
    ▮▮▮▮▮▮ 12.2.3 12.2.3 运行测试用例:Qt Creator 集成、命令行运行
    ▮▮▮▮▮▮ 12.2.4 12.2.4 数据驱动测试 (Data-Driven Testing)
    ▮▮▮▮ 12.3 12.3 GUI 程序调试技巧
    ▮▮▮▮▮▮ 12.3.1 12.3.1 断点调试:在 Qt Creator 中设置断点
    ▮▮▮▮▮▮ 12.3.2 12.3.2 日志输出:qDebug(), qWarning(), qCritical(), qFatal()
    ▮▮▮▮▮▮ 12.3.3 12.3.3 界面元素检查器 (Inspect):Qt Inspect 工具
    ▮▮▮▮▮▮ 12.3.4 12.3.4 内存泄漏检测工具:Valgrind, AddressSanitizer
    ▮▮ 13. 部署与发布:应用程序打包
    ▮▮▮▮ 13.1 13.1 应用程序部署概述
    ▮▮▮▮▮▮ 13.1.1 13.1.1 部署的目标平台:Windows, macOS, Linux
    ▮▮▮▮▮▮ 13.1.2 13.1.2 部署的类型:静态部署 (Static Deployment), 动态部署 (Dynamic Deployment)
    ▮▮▮▮▮▮ 13.1.3 13.1.3 依赖库 (Dependencies) 处理:Qt 库、第三方库
    ▮▮▮▮ 13.2 13.2 Windows 平台部署
    ▮▮▮▮▮▮ 13.2.1 13.2.1 动态部署:Qt DLLs 依赖
    ▮▮▮▮▮▮ 13.2.2 13.2.2 使用 windeployqt 工具自动部署 Qt 依赖
    ▮▮▮▮▮▮ 13.2.3 13.2.3 创建安装包:Inno Setup, NSIS
    ▮▮▮▮ 13.3 13.3 macOS 平台部署
    ▮▮▮▮▮▮ 13.3.1 13.3.1 应用程序包 (Application Bundle) 结构
    ▮▮▮▮▮▮ 13.3.2 13.3.2 动态部署:Qt Frameworks 依赖
    ▮▮▮▮▮▮ 13.3.3 13.3.3 使用 macdeployqt 工具自动部署 Qt 依赖
    ▮▮▮▮▮▮ 13.3.4 13.3.4 代码签名 (Code Signing) 与公证 (Notarization)
    ▮▮▮▮ 13.4 13.4 Linux 平台部署
    ▮▮▮▮▮▮ 13.4.1 13.4.1 动态部署:Qt 共享库 (Shared Libraries) 依赖
    ▮▮▮▮▮▮ 13.4.2 13.4.2 使用 ldd 命令查看依赖库
    ▮▮▮▮▮▮ 13.4.3 13.4.3 创建 AppImage, Snap, Flatpak 等通用 Linux 包格式
    ▮▮ 14. 实战案例:综合 GUI 应用开发
    ▮▮▮▮ 14.1 14.1 案例选择与需求分析
    ▮▮▮▮▮▮ 14.1.1 14.1.1 案例:简易文本编辑器
    ▮▮▮▮▮▮ 14.1.2 14.1.2 功能需求分析:文件操作、文本编辑、格式设置
    ▮▮▮▮▮▮ 14.1.3 14.1.3 界面设计:菜单栏、工具栏、文本编辑区域
    ▮▮▮▮ 14.2 14.2 项目搭建与界面布局
    ▮▮▮▮▮▮ 14.2.1 14.2.1 创建 Qt Widgets Application 项目
    ▮▮▮▮▮▮ 14.2.2 14.2.2 使用 Qt Designer 设计界面 (可选) 或代码布局
    ▮▮▮▮▮▮ 14.2.3 14.2.3 实现主窗口布局:QMainWindow, QMenuBar, QToolBar, QTextEdit
    ▮▮▮▮ 14.3 14.3 功能实现与事件处理
    ▮▮▮▮▮▮ 14.3.1 14.3.1 文件操作功能实现:新建、打开、保存、另存为
    ▮▮▮▮▮▮ 14.3.2 14.3.2 文本编辑功能实现:复制、粘贴、剪切、撤销、重做
    ▮▮▮▮▮▮ 14.3.3 14.3.3 格式设置功能实现:字体、字号、颜色
    ▮▮▮▮▮▮ 14.3.4 14.3.4 菜单项与工具栏按钮的事件响应
    ▮▮▮▮ 14.4 14.4 测试与优化
    ▮▮▮▮▮▮ 14.4.1 14.4.1 功能测试与 Bug 修复
    ▮▮▮▮▮▮ 14.4.2 14.4.2 性能优化:启动速度、内存占用、响应速度
    ▮▮▮▮▮▮ 14.4.3 14.4.3 用户体验优化:界面美观、操作流畅
    ▮▮ 15. 高级主题与进阶
    ▮▮▮▮ 15.1 15.1 自定义控件深度开发
    ▮▮▮▮▮▮ 15.1.1 15.1.1 复杂图形绘制与动画效果集成
    ▮▮▮▮▮▮ 15.1.2 15.1.2 自定义事件处理与手势识别
    ▮▮▮▮▮▮ 15.1.3 15.1.3 扩展属性系统:自定义属性编辑器 (Property Editor)
    ▮▮▮▮ 15.2 15.2 GUI 程序性能优化
    ▮▮▮▮▮▮ 15.2.1 15.2.1 渲染优化:硬件加速、减少绘制操作、缓存机制
    ▮▮▮▮▮▮ 15.2.2 15.2.2 内存优化:对象池、资源管理、避免内存泄漏
    ▮▮▮▮▮▮ 15.2.3 15.2.3 多线程优化:任务分解、线程池、异步操作
    ▮▮▮▮ 15.3 15.3 跨平台兼容性高级技巧
    ▮▮▮▮▮▮ 15.3.1 15.3.1 平台差异性分析:操作系统 API, 界面风格, 文件系统
    ▮▮▮▮▮▮ 15.3.2 15.3.2 条件编译 (Conditional Compilation) 与平台特性检测
    ▮▮▮▮▮▮ 15.3.3 15.3.3 统一界面风格:Qt Style Sheets, 平台原生风格
    ▮▮▮▮ 15.4 15.4 前沿技术趋势:Qt 6 新特性、现代 C++ GUI 开发
    ▮▮▮▮▮▮ 15.4.1 15.4.1 Qt 6 新特性:QML 引擎改进、图形渲染引擎 (RHI), C++ 模块更新
    ▮▮▮▮▮▮ 15.4.2 15.4.2 现代 C++ GUI 开发实践:CMake 构建系统、模块化设计、设计模式应用
    ▮▮▮▮▮▮ 15.4.3 15.4.3 GUI 开发未来趋势:WebAssembly, 跨平台框架演进
    ▮▮ 附录A: 附录 A:Qt 常用类速查表
    ▮▮ 附录B: 附录 B:C++ 基础知识回顾
    ▮▮ 附录C: 附录 C:参考文献与推荐阅读
    ▮▮ 附录D: 附录 D:术语表


    1. 起步:C++ GUI 编程概览

    1.1 什么是 GUI 编程?

    1.1.1 GUI 的定义与优势

    图形用户界面 (GUI, Graphical User Interface) 编程,指的是创建具有图形化用户界面的应用程序的过程。与传统的命令行界面 (CLI, Command-Line Interface) 不同,GUI 使用窗口、按钮、菜单、图标等图形元素,以直观可视的方式呈现信息,并允许用户通过鼠标、键盘等输入设备与程序进行交互。

    GUI 的核心目标是提升用户体验 (User Experience, UX)易用性 (Usability)。相较于需要记忆和输入命令的 CLI,GUI 提供了以下显著优势:

    直观易用: GUI 采用图形符号和可视化操作,用户无需记忆复杂的命令,通过点击、拖拽、选择等直观操作即可完成任务,学习成本大大降低。
    交互性强: GUI 支持丰富的交互方式,例如鼠标点击、键盘输入、拖放操作等,用户可以更自然、更灵活地与程序进行互动,操作效率更高。
    信息呈现丰富: GUI 可以使用文本、图像、动画、视频等多种媒体形式呈现信息,更生动、更形象,有助于用户快速理解和掌握信息。
    美观性: GUI 可以通过精心设计的界面布局、配色、图标等元素,提升应用程序的美观性,增强用户的视觉体验。
    广泛的应用领域: 从桌面应用程序、移动应用、Web 应用,到嵌入式系统、工业控制界面,GUI 编程几乎渗透到所有计算机应用领域。

    例如,日常使用的操作系统 (如 Windows、macOS、Linux 的桌面环境),以及各种应用程序(如浏览器、办公软件、图像处理软件、游戏等)都依赖于 GUI 技术。GUI 编程使得计算机应用不再局限于专业人士,而是能够被更广泛的用户群体轻松使用。

    1.1.2 GUI 编程的应用领域

    GUI 编程的应用领域极其广泛,几乎涵盖了所有需要人机交互的软件系统。以下列举一些主要的应用领域:

    桌面应用程序 (Desktop Applications): 这是 GUI 编程最经典的应用领域。各种操作系统上的桌面应用,如:
    ▮▮▮▮ⓑ 办公软件套件 (如 Microsoft Office, LibreOffice)
    ▮▮▮▮ⓒ 网页浏览器 (如 Chrome, Firefox, Safari)
    ▮▮▮▮ⓓ 图像处理软件 (如 Photoshop, GIMP)
    ▮▮▮▮ⓔ 视频编辑软件 (如 Adobe Premiere, DaVinci Resolve)
    ▮▮▮▮ⓕ 音乐播放器 (如 Spotify, iTunes)
    ▮▮▮▮ⓖ 代码编辑器 (如 VS Code, Sublime Text)
    ▮▮▮▮ⓗ 集成开发环境 (IDE, Integrated Development Environment) (如 Qt Creator, Visual Studio)

    这些应用程序都依赖于 GUI 来提供用户友好的操作界面,方便用户进行文档编辑、网页浏览、媒体处理、软件开发等各种任务。

    移动应用程序 (Mobile Applications): 智能手机和平板电脑上的应用,几乎全部都是 GUI 应用。例如:
    ▮▮▮▮ⓑ 社交媒体应用 (如 WeChat, Facebook, Twitter)
    ▮▮▮▮ⓒ 游戏 (如 王者荣耀, 原神, Candy Crush)
    ▮▮▮▮ⓓ 地图导航应用 (如 Google Maps, 百度地图, Apple Maps)
    ▮▮▮▮ⓔ 购物应用 (如 Taobao, Amazon, 京东)
    ▮▮▮▮ⓕ 移动办公应用 (如 WPS Office Mobile, Microsoft Office Mobile)

    移动应用的 GUI 设计更加注重触控操作和移动设备的特性,需要考虑屏幕尺寸、手势操作、电池续航等因素。

    嵌入式系统 (Embedded Systems): 许多嵌入式设备也配备了 GUI,以提供本地的人机交互界面。例如:
    ▮▮▮▮ⓑ 智能家居设备 (如 智能音箱、智能面板)
    ▮▮▮▮ⓒ 工业控制面板 (如 PLC 控制器、HMI 人机界面)
    ▮▮▮▮ⓓ 汽车电子系统 (如 车载信息娱乐系统、仪表盘)
    ▮▮▮▮ⓔ 医疗设备 (如 监护仪、诊断仪器)
    ▮▮▮▮ⓕ 智能穿戴设备 (如 智能手表、智能手环)

    嵌入式系统的 GUI 通常需要考虑资源限制(如内存、处理器性能),并针对特定的应用场景进行定制化设计。

    Web 应用程序 (Web Applications): 虽然 Web 应用主要通过浏览器访问,但现代 Web 应用也大量使用了 GUI 技术,例如:
    ▮▮▮▮ⓑ 在线办公套件 (如 Google Docs, 腾讯文档)
    ▮▮▮▮ⓒ Web 版图像编辑器 (如 Pixlr, Photopea)
    ▮▮▮▮ⓓ 各种 Web 管理后台 (如 网站管理后台、服务器管理面板)
    ▮▮▮▮ⓔ 基于 Web 的 IDE (如 CodeSandbox, Gitpod)

    Web GUI 开发通常使用 HTML, CSS, JavaScript 等 Web 技术,以及各种 Web GUI 框架 (如 React, Vue, Angular)。

    游戏 (Games): 电子游戏是 GUI 编程的另一个重要领域。游戏 GUI 不仅用于游戏菜单、设置界面,也用于游戏内的 HUD (Heads-Up Display) 信息显示、角色控制界面等。游戏 GUI 设计通常需要考虑游戏的艺术风格、操作的流畅性、以及性能优化。

    总而言之,GUI 编程几乎无处不在,它是现代软件开发不可或缺的一部分,也是构建用户友好、功能强大的应用程序的关键技术。

    1.1.3 GUI 编程的核心概念:事件驱动

    GUI 编程与传统的线性程序设计最大的区别在于其事件驱动 (Event-driven) 的编程模型。在传统的 CLI 程序中,程序流程由代码的执行顺序决定,程序主动发起操作,用户被动响应。而在 GUI 程序中,程序流程主要由用户操作系统事件驱动,程序被动地响应外部事件,并执行相应的处理逻辑。

    事件 (Event) 可以理解为程序运行时发生的事情,例如:

    用户输入事件: 鼠标点击、鼠标移动、键盘按键、触摸屏幕等。
    窗口事件: 窗口创建、窗口关闭、窗口大小改变、窗口最小化/最大化等。
    定时器事件: 定时器到期。
    网络事件: 网络数据到达、网络连接建立/断开。
    操作系统事件: 系统消息、硬件状态变化等。

    事件驱动模型 的核心思想是:

    1. 事件循环 (Event Loop): GUI 程序启动后,会进入一个无限循环,不断地监听和处理各种事件。这个循环称为事件循环。
    2. 事件队列 (Event Queue): 当事件发生时,操作系统会将事件放入事件队列中。
    3. 事件处理 (Event Handling): 事件循环从事件队列中取出事件,并根据事件类型和目标对象,调用相应的事件处理函数 (Event Handler)槽函数 (Slot Function) 来处理事件。
    4. 程序响应: 事件处理函数执行相应的逻辑,例如更新界面、执行计算、发送网络请求等,从而对事件做出响应。

    举例说明

    假设用户点击了 GUI 界面上的一个按钮 (Button)。

    1. 事件发生: 鼠标点击事件发生,操作系统检测到鼠标点击操作发生在按钮控件上。
    2. 事件入队: 操作系统将鼠标点击事件放入应用程序的事件队列中。
    3. 事件循环: GUI 程序的事件循环不断运行,从事件队列中取出鼠标点击事件。
    4. 事件处理: 事件循环根据事件信息,找到按钮控件,并调用与按钮点击事件关联的事件处理函数或槽函数。
    5. 程序响应: 事件处理函数被执行,例如弹出一个对话框、执行某个功能、或者更新界面显示。

    这种事件驱动的编程模型使得 GUI 程序能够异步 (Asynchronous) 地响应用户的操作和系统事件,保持界面的响应性 (Responsiveness)交互性。用户可以在任何时候进行操作,程序都能及时响应,不会出现卡顿或无响应的情况。

    理解事件驱动模型是 GUI 编程的基础,也是掌握 Qt 框架的关键。Qt 框架提供了强大的事件处理机制,包括信号与槽 (Signals and Slots) 机制,使得事件处理更加灵活、高效、易于维护。在后续章节中,我们将深入学习 Qt 的事件处理机制。

    1.2 C++ 与 GUI 开发

    1.2.1 C++ 的优势:性能与控制力

    C++ 是一种强大的通用编程语言 (General-purpose Programming Language),长期以来在系统软件、应用程序开发、游戏开发等领域占据着重要地位。在 GUI 开发领域,C++ 同样拥有独特的优势:

    卓越的性能 (Performance): C++ 是一种编译型语言 (Compiled Language),代码直接编译成机器码执行,执行效率非常高。这对于 GUI 应用程序尤为重要,因为 GUI 程序通常需要处理大量的图形渲染、用户交互、以及复杂的计算任务。C++ 的高性能可以保证 GUI 程序的流畅性 (Smoothness)响应速度 (Responsiveness),尤其是在开发高性能图形应用、游戏、以及资源密集型应用时,C++ 的性能优势更加明显。

    强大的控制力 (Control): C++ 提供了对硬件资源 (Hardware Resources) 的精细控制能力,例如内存管理、多线程并发、底层系统调用等。这使得开发者可以充分利用计算机的硬件性能,进行底层优化,满足 GUI 程序对性能和资源控制的苛刻要求。尤其是在开发嵌入式 GUI 应用、高性能服务器应用、以及需要直接操作硬件的应用时,C++ 的控制力至关重要。

    丰富的库和框架 (Libraries and Frameworks): C++ 拥有庞大而成熟的标准库 (Standard Library)第三方库生态系统 (Third-party Library Ecosystem),提供了丰富的工具和组件,可以大大提高开发效率。在 GUI 开发领域,有许多优秀的 C++ GUI 框架可供选择,例如 Qt, wxWidgets, GTK+, MFC 等。这些框架提供了丰富的 GUI 组件、工具函数、以及跨平台支持,使得 C++ 开发者可以快速构建功能强大的 GUI 应用程序。

    跨平台能力 (Cross-platform Capability): C++ 具有良好的跨平台能力。通过使用跨平台 GUI 框架,C++ 应用程序可以轻松地在不同的操作系统 (如 Windows, macOS, Linux, Android, iOS) 上编译和运行,而无需修改大量代码。这大大降低了跨平台开发的成本和复杂性,提高了代码的可移植性和可维护性。Qt 框架就是 C++ 跨平台能力的杰出代表,它支持几乎所有主流的操作系统平台。

    成熟的生态系统和社区支持 (Mature Ecosystem and Community Support): C++ 是一门非常成熟的编程语言,拥有庞大的开发者社区和活跃的技术生态系统。这意味着 C++ 开发者可以很容易地找到学习资源、开发工具、以及技术支持。在 GUI 开发领域,Qt 框架也拥有非常活跃的社区,提供了丰富的文档、示例代码、论坛、以及商业支持,为 C++ GUI 开发提供了坚实的基础。

    尽管 C++ 在 GUI 开发领域具有诸多优势,但也需要认识到 C++ 的一些挑战,例如:

    学习曲线陡峭 (Steep Learning Curve): C++ 是一门相对复杂的语言,学习曲线较陡峭,需要掌握较多的概念和技术。
    内存管理复杂 (Complex Memory Management): C++ 需要手动进行内存管理,容易出现内存泄漏、野指针等问题,需要开发者具备良好的内存管理意识和技巧。
    编译时间较长 (Longer Compilation Time): C++ 是编译型语言,编译时间相对较长,尤其是在大型项目中。

    然而,对于追求高性能、高控制力、跨平台 GUI 应用的开发者来说,C++ 仍然是首选的编程语言之一。通过选择合适的 GUI 框架,例如本书重点介绍的 Qt 框架,可以有效地克服 C++ GUI 开发的挑战,充分发挥 C++ 的优势,构建出色的 GUI 应用程序。

    1.2.2 C++ GUI 框架的选择

    在 C++ GUI 开发领域,有多种成熟的 GUI 框架可供选择。不同的框架具有不同的特点、优势和适用场景。以下简要介绍几种常见的 C++ GUI 框架,并引出本书重点介绍的 Qt 框架:

    Qt (Qt): Qt 是一个跨平台 (Cross-platform) 的应用程序开发框架,由 Qt Company 开发和维护。Qt 不仅是一个 GUI 框架,更是一个全面的应用程序开发平台,提供了丰富的库和工具,涵盖了 GUI、网络、数据库、多媒体、XML、WebKit、OpenGL 等各个方面。Qt 使用 C++ 语言开发,但也支持 QML (Qt Meta-Object Language) 声明式 UI 语言,可以灵活地构建现代化的用户界面。Qt 以其跨平台性模块化易用性强大的功能活跃的社区而著称,是目前最流行的 C++ GUI 框架之一,被广泛应用于桌面应用、移动应用、嵌入式系统等领域。本书将重点介绍 Qt 框架。

    wxWidgets (wxWidgets): wxWidgets 是另一个流行的开源 (Open-source) 跨平台 C++ GUI 框架。wxWidgets 的特点是原生外观 (Native Look and Feel),它使用各个操作系统平台原生的 GUI 组件来构建界面,使得应用程序在不同平台上具有一致的用户体验,更符合平台规范。wxWidgets 框架相对轻量级,学习曲线较为平缓,适合开发需要原生外观的跨平台 GUI 应用。

    GTK+ (GTK, GIMP Toolkit): GTK+ 最初是为 GIMP (GNU Image Manipulation Program) 图像处理软件开发的 GUI 框架,后来发展成为一个通用的跨平台 GUI 工具包。GTK+ 主要用于 Linux 平台,是 Linux 桌面环境 (如 GNOME, Xfce) 的基础 GUI 框架。GTK+ 也支持 Windows 和 macOS 平台,但原生外观在非 Linux 平台下可能不如 wxWidgets。GTK+ 使用 C 语言开发,提供了 C++ 绑定 (GTKmm)。

    MFC (Microsoft Foundation Class Library): MFC 是微软公司提供的Windows 平台 C++ GUI 框架。MFC 基于 Windows API 构建,紧密集成于 Windows 操作系统,提供了丰富的 Windows GUI 组件和功能。MFC 主要用于开发 Windows 平台下的桌面应用程序,与 Visual Studio IDE 集成良好。但 MFC 仅限于 Windows 平台,不具备跨平台能力,且技术相对陈旧,在跨平台 GUI 开发领域逐渐式微。

    Win32 API (Windows API): Win32 API 是 Windows 操作系统提供的原生 (Native) C/C++ API,可以直接调用 Windows 操作系统提供的 GUI 函数来构建界面。Win32 API 功能强大、性能高效,但编程接口较为底层,开发效率较低,代码可移植性差,通常只在需要极致性能或特定 Windows 系统功能时才考虑直接使用 Win32 API 进行 GUI 开发。

    Cocoa/AppKit (Cocoa/AppKit): Cocoa/AppKit 是 macOS 和 iOS 平台上的原生 Objective-C/Swift GUI 框架。虽然 Cocoa/AppKit 主要使用 Objective-C 或 Swift 语言开发,但也可以通过 C++ 进行互操作。如果需要开发 macOS 或 iOS 平台下的原生 GUI 应用,并充分利用平台特性,可以考虑使用 Cocoa/AppKit 框架。

    框架选择建议

    跨平台需求: 如果需要开发跨平台 GUI 应用程序,Qt, wxWidgets, GTK+ 是主要的选择。Qt 和 wxWidgets 在跨平台能力和易用性方面更具优势。
    原生外观: 如果追求应用程序在不同平台上的原生外观,wxWidgets 是一个不错的选择。
    Linux 平台: 如果主要面向 Linux 平台开发,GTK+ 是一个成熟且广泛使用的框架。
    Windows 平台: 如果只针对 Windows 平台开发,MFC 或 Win32 API 可以作为备选项,但 Qt 也完全胜任 Windows 平台开发,并且具备跨平台能力。
    现代 UI 和丰富功能: 如果需要构建现代化的、功能丰富的 GUI 应用程序,Qt 框架是最佳选择,它提供了强大的 QML 声明式 UI 语言、丰富的 GUI 组件、以及完善的工具链。
    学习曲线和开发效率: Qt 框架虽然功能强大,但学习曲线相对平缓,开发效率较高。wxWidgets 和 GTK+ 的学习曲线也相对较平缓。MFC 和 Win32 API 的学习曲线则较为陡峭,开发效率较低。

    本书选择 Qt 框架 作为重点介绍对象,主要是因为 Qt 在跨平台性功能性易用性社区支持等方面都表现出色,是目前最主流、最强大的 C++ GUI 框架之一,也是学习 C++ GUI 编程的最佳选择。在后续章节中,我们将深入学习 Qt 框架的各个方面,掌握使用 Qt 进行高效 GUI 应用开发的关键技能。

    1.3 为什么选择 Qt 框架?

    1.3.1 Qt 的特性与优势:跨平台、模块化、易用性

    Qt 框架之所以在众多 C++ GUI 框架中脱颖而出,成为业界翘楚,得益于其独特而强大的特性和优势。以下详细阐述 Qt 的核心特性和优势:

    卓越的跨平台性 (Cross-platform): 跨平台是 Qt 最核心的特性之一,也是其最大的优势。Qt 框架可以无缝 (Seamlessly) 运行在几乎所有主流的操作系统平台上,包括:

    桌面平台: Windows, macOS, Linux (包括各种发行版), X11, Wayland
    移动平台: Android, iOS, watchOS, tvOS
    嵌入式平台: QNX, INTEGRITY, VxWorks, embedded Linux, 以及各种微控制器平台 (通过 Qt for MCU)

    Qt 的跨平台性并非简单的代码移植,而是真正意义上的跨平台。Qt 框架在底层抽象 (Abstract) 了各个操作系统平台的差异,提供了一套统一的 API (应用程序编程接口),开发者只需要编写一套代码,就可以在不同的平台上编译和运行,无需为每个平台单独编写代码。Qt 甚至可以做到二进制兼容 (Binary Compatibility),即在某些平台上,同一个 Qt 程序可以直接在不同的操作系统版本之间运行,无需重新编译。

    Qt 的跨平台性为开发者带来了巨大的便利:

    降低开发成本: 只需维护一套代码,大大减少了跨平台开发的成本和工作量。
    提高开发效率: 开发者可以专注于业务逻辑的实现,而无需花费大量时间处理平台差异性问题。
    扩大市场覆盖: 应用程序可以轻松地部署到多个平台,覆盖更广泛的用户群体。
    保护投资: 应用程序的代码可以长期使用,即使未来需要迁移到新的平台,也无需从头开始重写。

    高度模块化 (Modularization): Qt 框架采用模块化设计 (Modular Design),将不同的功能划分为独立的模块,例如:

    QtCore: 核心模块,提供核心的非 GUI 功能,如对象模型、信号与槽、事件处理、文件系统、线程、XML、JSON、国际化等。
    QtGui: 图形界面基础模块,提供窗口系统、事件处理、2D 图形、字体、文本、图像、打印等 GUI 基础功能。
    QtWidgets: 经典控件模块,提供各种传统的 GUI 控件,如按钮、标签、文本框、列表框、树形控件、表格控件等。
    QtNetwork: 网络编程模块,提供 TCP/UDP 套接字、HTTP、SSL/TLS 等网络功能。
    QtSql: 数据库访问模块,提供 SQL 数据库的连接、查询、事务处理等功能,支持 SQLite, MySQL, PostgreSQL 等多种数据库。
    QtMultimedia: 多媒体模块,提供音频、视频的播放、录制、处理等功能。
    QtOpenGL: OpenGL 集成模块,提供 OpenGL 渲染支持,用于 2D/3D 图形渲染。
    QtWebEngine: 基于 Chromium 的 Web 引擎模块,用于嵌入 Web 浏览器功能。
    QtQuick: 现代 UI 模块,提供 QML 声明式 UI 语言和 Qt Quick 场景图渲染引擎,用于构建现代、流畅的用户界面。

    Qt 的模块化设计使得开发者可以按需选择 (Choose on Demand) 所需的模块,灵活地构建 (Flexibly Build) 应用程序。开发者可以只包含应用程序需要的模块,减少程序体积 (Reduce Program Size)提高程序性能 (Improve Program Performance)降低资源占用 (Reduce Resource Consumption)。同时,模块化设计也使得 Qt 框架本身更易于维护和扩展。

    强大的功能 (Powerful Features): Qt 框架提供了极其丰富 (Extremely Rich) 的功能,几乎涵盖了应用程序开发的各个方面 (Every Aspect)。除了前面提到的模块功能,Qt 还提供了:

    信号与槽 (Signals and Slots): Qt 独创的对象间通信机制 (Object-to-object Communication Mechanism),使得对象之间的交互更加灵活、安全、易于维护。
    元对象系统 (Meta-Object System): Qt 的核心机制,为 Qt 对象提供了反射 (Reflection)动态属性 (Dynamic Properties)运行时类型信息 (Run-Time Type Information, RTTI) 等高级特性,是信号与槽、属性系统等功能的基础。
    对象模型 (Object Model): Qt 的对象模型基于 C++ 的多重继承 (Multiple Inheritance)元对象系统构建,提供了对象树 (Object Tree)自动内存管理 (Automatic Memory Management) 等特性,简化了对象生命周期管理和内存管理。
    布局管理 (Layout Management): Qt 提供了强大的布局管理器系统 (Layout Manager System),可以自动调整控件的大小和位置,适应窗口大小变化,使得 GUI 界面设计更加灵活、高效。
    样式表 (Style Sheets): Qt 支持CSS 风格的样式表 (CSS-style Style Sheets),可以方便地定制 GUI 界面的外观,实现界面美化和风格统一。
    国际化 (Internationalization, i18n) 和本地化 (Localization, l10n): Qt 提供了完善的国际化和本地化支持,可以轻松地将应用程序翻译成不同的语言,适应不同国家和地区的用户。
    文档和帮助系统 (Documentation and Help System): Qt 拥有完善的文档 (Comprehensive Documentation)帮助系统,包括 API 文档、教程、示例代码、在线帮助等,为开发者提供了丰富的学习资源和技术支持。

    易用性 (Ease of Use): 尽管 Qt 功能强大,但其 API 设计却非常友好 (Friendly)直观 (Intuitive)易于使用 (Easy to Use)。Qt 的 API 设计遵循一致性原则 (Consistency Principle),命名规范清晰,类和函数的功能易于理解和记忆。Qt 提供了大量的示例代码 (Example Code)教程 (Tutorials),帮助开发者快速上手。Qt Creator IDE 也提供了可视化设计器 (Visual Designer)代码自动完成 (Code Auto-completion)调试器 (Debugger) 等强大的开发工具,进一步提高了开发效率。

    活跃的社区和强大的商业支持 (Active Community and Strong Commercial Support): Qt 拥有一个庞大 (Huge)活跃 (Active)开发者社区 (Developer Community),全球有数百万 Qt 开发者。Qt 社区提供了丰富的资源,包括论坛、邮件列表、博客、Wiki、示例代码、开源项目等,开发者可以在社区中互相交流、学习、分享经验、解决问题。Qt 框架由 Qt Company 商业公司开发和维护,提供了专业的商业支持 (Professional Commercial Support),包括技术咨询、培训、授权许可、定制开发等服务,为企业级 Qt 应用开发提供了可靠的保障。

    综上所述,Qt 框架以其跨平台性模块化强大的功能易用性活跃的社区强大的商业支持,成为 C++ GUI 开发的首选框架,也是本书重点介绍和深入解析的对象。

    1.3.2 Qt 的工具链:Qt Creator, qmake, CMake

    Qt 框架提供了一套完整 (Complete)工具链 (Toolchain),辅助开发者进行高效的 Qt 应用程序开发。Qt 工具链主要包括以下核心组件:

    Qt Creator (Qt Creator): Qt Creator 是 Qt 官方提供的集成开发环境 (IDE, Integrated Development Environment)。Qt Creator 专为 Qt 开发而设计,集成了代码编辑器、可视化设计器、构建工具、调试器、版本控制、性能分析等功能于一体,为 Qt 开发者提供了一站式的开发体验。

    Qt Creator 的主要特点包括:

    代码编辑器 (Code Editor): 支持 C++, QML, JavaScript 等语言,提供代码高亮、代码自动完成、代码导航、代码重构等功能,提高代码编写效率。
    可视化设计器 (Qt Designer): 提供拖拽式 (Drag-and-drop) 的 GUI 界面设计器,可以可视化地设计 Qt Widgets 和 Qt Quick 界面,无需手动编写大量代码,大大简化了界面设计过程。
    构建工具集成 (Build Tool Integration): 内置 qmake 和 CMake 构建工具的支持,可以方便地配置和管理 Qt 项目的构建过程。
    调试器 (Debugger): 集成 C++ 和 QML 调试器,支持断点调试、单步执行、变量查看、调用堆栈分析等功能,帮助开发者快速定位和解决程序 Bug。
    版本控制 (Version Control): 集成 Git, Subversion, Perforce 等版本控制系统,方便团队协作和代码管理。
    性能分析 (Performance Analysis): 集成性能分析工具,可以分析程序的性能瓶颈,帮助开发者进行性能优化。
    Qt 文档集成 (Qt Documentation Integration): 内置 Qt 官方文档,可以方便地查阅 Qt API 文档和帮助信息。
    跨平台支持 (Cross-platform Support): Qt Creator 本身也是一个 Qt 应用程序,可以运行在 Windows, macOS, Linux 等多个平台上,与 Qt 框架的跨平台特性保持一致。

    Qt Creator 是 Qt 开发的首选 IDE (Preferred IDE),也是本书推荐使用的开发工具。

    qmake (qmake): qmake 是 Qt 官方提供的构建工具 (Build Tool),用于自动化 (Automate) Qt 项目的构建过程 (Build Process)。qmake 使用 项目文件 (.pro 文件) 来描述项目的构建配置,包括源文件、头文件、库依赖、编译选项、链接选项等。qmake 可以根据项目文件生成 Makefile,然后使用 make 命令 (或 nmake 在 Windows 平台) 进行编译和链接,最终生成可执行文件或库文件。

    qmake 的主要特点包括:

    简单易用 (Simple and Easy to Use): qmake 的项目文件语法简洁明了,易于学习和使用。
    跨平台性 (Cross-platform): qmake 可以生成适用于不同操作系统平台的 Makefile,实现跨平台构建。
    Qt 集成 (Qt Integration): qmake 深度集成于 Qt 框架,可以自动处理 Qt 模块依赖、MOC (元对象编译器) 处理、资源文件处理等 Qt 特有构建步骤。
    灵活可配置 (Flexible and Configurable): qmake 提供了丰富的配置选项,可以定制项目的构建过程,例如添加自定义编译选项、链接选项、预处理宏、资源文件等。

    qmake 是 Qt 早期版本的主要构建工具,也是很多 Qt 项目仍然使用的构建系统。

    CMake (CMake): CMake 是一个开源 (Open-source)跨平台 (Cross-platform)构建系统生成工具 (Build System Generator)。CMake 本身并不直接进行编译和链接,而是根据 CMakeLists.txt 文件 生成 平台原生的构建系统文件,例如 Makefile (Linux, macOS), Visual Studio 工程文件 (Windows), Xcode 工程文件 (macOS) 等。然后,开发者可以使用平台原生的构建工具 (如 make, Visual Studio, Xcode) 进行编译和链接。

    CMake 的主要特点包括:

    跨平台性 (Cross-platform): CMake 可以生成适用于多种操作系统和构建工具的构建系统文件,实现真正的跨平台构建。
    强大灵活 (Powerful and Flexible): CMake 提供了强大的配置能力,可以处理复杂的构建需求,例如多语言项目、多平台项目、复杂的依赖关系、自定义构建步骤等。
    现代构建系统 (Modern Build System): CMake 是一个现代化的构建系统,支持现代 C++ 开发的最佳实践,例如模块化设计、组件化构建、测试集成等。
    广泛应用 (Widely Used): CMake 被广泛应用于开源项目和商业项目,是目前最流行的 C++ 构建系统之一。

    近年来,CMake 逐渐成为 Qt 项目的主流构建系统 (Mainstream Build System)。Qt 官方也开始推荐使用 CMake 构建 Qt 项目,Qt 6 及以后的版本对 CMake 的支持更加完善。CMake 相比 qmake 更加强大和灵活,更适合大型、复杂的 Qt 项目。

    工具链选择建议

    初学者: 对于初学者,建议使用 Qt Creator IDEqmake 构建系统。Qt Creator 提供了友好的可视化界面和集成开发环境,qmake 简单易用,学习曲线平缓,可以快速上手 Qt 开发。
    中高级开发者: 对于中高级开发者,建议逐步过渡到 CMake 构建系统。CMake 虽然学习曲线稍陡峭,但功能更加强大和灵活,更适合大型、复杂的 Qt 项目,也更符合现代 C++ 开发的趋势。Qt Creator IDE 也完全支持 CMake 构建系统。
    小型项目: 对于小型 Qt 项目,qmake 和 CMake 都可以胜任。qmake 更加轻量级,配置简单,适合快速开发和原型验证。
    大型项目: 对于大型 Qt 项目,CMake 更加适合。CMake 可以更好地管理项目的复杂性,提高构建效率,支持模块化和组件化构建。

    本书主要使用 Qt Creator IDE 进行开发示例,构建系统根据章节内容和项目复杂度,可能使用 qmake 或 CMake,并在相关章节中进行说明。

    1.3.3 Qt 的社区与资源

    Qt 框架之所以能够蓬勃发展,并被广泛应用于各行各业,除了其自身的技术优势外,还离不开活跃 (Active)社区 (Community)丰富 (Rich)资源 (Resources)。Qt 社区和资源为 Qt 开发者提供了强大的支持,加速了学习过程,提升了开发效率,保障了项目成功。

    官方文档 (Official Documentation): Qt 官方提供了极其完善 (Extremely Comprehensive)文档 (Documentation),是学习 Qt 最权威、最可靠的资源。Qt 官方文档包括:

    API 文档 (API Reference): 详细描述了 Qt 框架中所有类、函数、宏、枚举等的用法、参数、返回值、示例代码等。API 文档是学习 Qt API 的最直接、最准确的资料。
    教程 (Tutorials): 提供各种 Qt 功能的入门教程,例如 GUI 编程、信号与槽、布局管理、模型/视图架构、网络编程、数据库编程、Qt Quick、QML 等。教程通过实例演示,帮助初学者快速上手 Qt 开发。
    示例 (Examples): 包含大量的 Qt 示例代码,涵盖了 Qt 框架的各种功能和应用场景。示例代码是学习 Qt 功能的最佳实践,可以直接运行和修改示例代码,快速理解 Qt 的用法。
    指南 (Guides): 提供各种 Qt 开发的指南和最佳实践,例如 Qt 编程风格指南、性能优化指南、跨平台开发指南、部署指南等。指南帮助开发者编写高质量、高性能、可维护的 Qt 应用程序。
    Qt Wiki (Qt Wiki): Qt 社区维护的 Wiki 网站,包含大量的 Qt 知识、技巧、FAQ、贡献文章等。Qt Wiki 是一个开放的知识库,开发者可以在 Wiki 上分享知识、解决问题、互相学习。

    Qt 官方文档质量非常高,内容详尽、示例丰富、更新及时,是学习 Qt 必不可少的资源。Qt Creator IDE 也集成了 Qt 官方文档,可以方便地在线或离线查阅文档。

    Qt 社区论坛 (Qt Community Forums): Qt 社区论坛是 Qt 开发者交流和互助的重要平台。Qt 官方论坛 (forum.qt.io) 和 Stack Overflow (stackoverflow.com) 上都有大量的 Qt 相关问题和解答。开发者可以在论坛上提问、解答问题、分享经验、交流技术。Qt 社区论坛非常活跃,通常能够得到及时、专业的解答。

    Qt 示例代码库 (Qt Example Code Repository): Qt 官方和社区维护了大量的 Qt 示例代码库,例如 Qt Examples (Qt 官方示例), Qt Demos (Qt 官方演示程序), Qt Gallery (Qt 社区示例库) 等。这些示例代码库涵盖了 Qt 框架的各种功能和应用场景,是学习 Qt 功能和技巧的宝贵资源。开发者可以通过阅读和运行示例代码,快速掌握 Qt 的用法,并借鉴示例代码解决实际开发问题。

    Qt 开源项目 (Qt Open Source Projects): Qt 框架本身就是开源的,基于 LGPLv3 和 GPLv2/v3 双重许可 (Dual Licensing)。除了 Qt 框架本身,Qt 社区还涌现出大量的 Qt 开源项目,例如各种 Qt 扩展库、工具、组件、应用程序等。这些开源项目为 Qt 开发者提供了丰富的选择,可以直接使用或参考开源项目,加速开发进程,提高代码质量。

    Qt 开发者博客 (Qt Developer Blogs): 许多 Qt 开发者、Qt 公司员工、Qt 专家都维护了自己的博客,分享 Qt 技术文章、经验总结、最佳实践、新特性介绍等。通过阅读 Qt 开发者博客,可以及时了解 Qt 最新技术动态、学习高级 Qt 技巧、深入理解 Qt 框架原理。

    Qt 学习书籍和教程 (Qt Learning Books and Tutorials): 市面上出版了大量的 Qt 学习书籍和在线教程,涵盖了 Qt 的各个方面,例如 Qt 入门、GUI 编程、QML 开发、网络编程、数据库编程、高级 Qt 技术等。这些书籍和教程以系统化的方式讲解 Qt 知识,帮助读者系统学习 Qt 框架,从入门到精通。本书也旨在为 Qt 学习者提供一本全面、深入、实用的 Qt GUI 编程指南。

    Qt 商业支持 (Qt Commercial Support): Qt Company 提供了专业的商业支持服务,包括技术咨询、培训、授权许可、定制开发等。对于企业级 Qt 应用开发,Qt 商业支持可以提供可靠的技术保障和商业合作。

    学习资源建议

    首选: Qt 官方文档 (doc.qt.io) - 最权威、最全面的学习资源,学习 Qt 必读。
    入门: Qt 教程 (Qt Tutorials) - 通过实例演示,快速上手 Qt 开发。
    实践: Qt 示例代码 (Qt Examples, Qt Demos) - 学习 Qt 功能的最佳实践,直接运行和修改示例代码。
    交流: Qt 社区论坛 (forum.qt.io, Stack Overflow) - 提问、解答问题、分享经验、交流技术。
    深入: Qt 开发者博客 (Qt Developer Blogs), Qt 学习书籍 (Qt Learning Books) - 了解 Qt 最新技术动态、学习高级 Qt 技巧、深入理解 Qt 框架原理。

    通过充分利用 Qt 社区和资源,开发者可以更高效地学习和掌握 Qt 框架,构建出色的 Qt 应用程序。

    1.4 搭建 Qt 开发环境

    1.4.1 Windows 环境下的 Qt 安装与配置

    在 Windows 操作系统下搭建 Qt 开发环境,主要包括以下步骤:

    下载 Qt SDK (Software Development Kit, 软件开发工具包):

    ⚝ 访问 Qt 官网下载页面:https://www.qt.io/offline-installers
    ⚝ 找到 "Qt Online Installers" 部分,选择 "Download the Qt Online Installer for Windows"
    ⚝ 下载 Qt Online Installer 在线安装程序 (例如 qt-unified-windows-x86-online.exe)。

    运行 Qt Online Installer 安装程序:

    ⚝ 双击运行下载的安装程序 qt-unified-windows-x86-online.exe
    ⚝ 按照安装向导的提示进行操作。

    登录 Qt 账号 (可选,但推荐):

    ⚝ 安装程序启动后,会提示登录 Qt 账号。
    ⚝ 如果没有 Qt 账号,可以免费注册一个。
    ⚝ 登录 Qt 账号可以享受 Qt 的完整功能和许可,推荐登录。

    选择 Qt 版本和组件:

    ⚝ 在 "Select Components" 页面,选择要安装的 Qt 版本和组件。
    Qt 版本: 建议选择最新的 LTS (Long Term Support, 长期支持) 版本,例如 Qt 6.x LTS。
    组件:
    ▮▮▮▮⚝ Qt Modules (Qt 模块): 根据需要选择 Qt 模块,通常需要选择:
    ▮▮▮▮▮▮▮▮⚝ Qt Core (QtCore)
    ▮▮▮▮▮▮▮▮⚝ Qt GUI (QtGui)
    ▮▮▮▮▮▮▮▮⚝ Qt Widgets (QtWidgets)
    ▮▮▮▮▮▮▮▮⚝ Qt Network (QtNetwork)
    ▮▮▮▮▮▮▮▮⚝ Qt SQL (QtSql)
    ▮▮▮▮▮▮▮▮⚝ Qt Multimedia (QtMultimedia)
    ▮▮▮▮▮▮▮▮⚝ Qt Quick (QtQuick)
    ▮▮▮▮▮▮▮▮⚝ Qt WebEngine (QtWebEngine)
    ▮▮▮▮▮▮▮▮⚝ ... (根据项目需求选择其他模块)
    ▮▮▮▮⚝ Qt Tools (Qt 工具): 必须选择:
    ▮▮▮▮▮▮▮▮⚝ Qt Creator (Qt Creator IDE)
    ▮▮▮▮▮▮▮▮⚝ Qt 6.x.x: 选择与 Qt 版本对应的构建工具链,例如:
    ▮▮▮▮▮▮▮▮▮▮▮▮⚝ MinGW: MinGW (Minimalist GNU for Windows) 是 Windows 平台下的 GCC (GNU Compiler Collection) 编译器套件,用于编译 C++ 代码。建议选择与 Qt 版本兼容的 MinGW 版本。
    ▮▮▮▮▮▮▮▮▮▮▮▮⚝ MSVC: MSVC (Microsoft Visual C++) 是 Microsoft Visual Studio 提供的 C++ 编译器套件。如果已经安装了 Visual Studio,可以选择 MSVC 构建工具链。
    ▮▮▮▮⚝ Examples (示例): 推荐选择,包含大量的 Qt 示例代码,方便学习。
    ▮▮▮▮⚝ Sources (源码): 可选,如果需要查看 Qt 源码,可以选择安装。

    注意: 选择组件越多,安装时间越长,占用磁盘空间越大。建议根据实际需求选择必要的组件。

    选择安装路径:

    ⚝ 在 "Installation Folder" 页面,选择 Qt SDK 的安装路径。
    ⚝ 建议选择一个不包含中文和空格的路径,例如 C:\Qt\Qt6.x.x

    完成安装:

    ⚝ 点击 "Install" 按钮,开始安装 Qt SDK。
    ⚝ 安装过程可能需要一段时间,取决于选择的组件和网络速度。
    ⚝ 安装完成后,点击 "Finish" 按钮,完成安装。

    配置环境变量 (可选,但推荐):

    ⚝ 将 Qt 的 bin 目录 添加到系统环境变量 Path 中,方便在命令行中使用 Qt 工具 (例如 qmake, cmake, windeployqt 等)。
    ⚝ 例如,如果 Qt 安装在 C:\Qt\Qt6.x.x\mingw_64,则将 C:\Qt\Qt6.x.x\mingw_64\bin 添加到 Path 环境变量。
    操作步骤:
    ▮▮▮▮⚝ 在 Windows 搜索栏搜索 "环境变量",打开 "编辑系统环境变量"。
    ▮▮▮▮⚝ 点击 "环境变量" 按钮。
    ▮▮▮▮⚝ 在 "系统变量" 部分,找到 "Path" 变量,点击 "编辑"。
    ▮▮▮▮⚝ 点击 "新建" 按钮,添加 Qt 的 bin 目录路径 (例如 C:\Qt\Qt6.x.x\mingw_64\bin)。
    ▮▮▮▮⚝ 点击 "确定" 保存修改。

    启动 Qt Creator:

    ⚝ 安装完成后,可以在开始菜单中找到 Qt Creator 快捷方式,启动 Qt Creator IDE。
    ⚝ 首次启动 Qt Creator,可能会提示配置 Kit (套件)。Kit 包含了 Qt 版本、编译器、调试器等信息。Qt Creator 通常会自动检测并配置 Kit。
    ⚝ 可以打开 Qt Creator 的 "工具 (Tools)" -> "选项 (Options)" -> "Kit" -> "构建套件 (Kits)" 页面,查看和配置 Kit。

    至此,Windows 环境下的 Qt 开发环境搭建完成。可以开始使用 Qt Creator 创建和开发 Qt 应用程序了。

    1.4.2 macOS 环境下的 Qt 安装与配置

    在 macOS 操作系统下搭建 Qt 开发环境,主要包括以下步骤:

    下载 Qt SDK:

    ⚝ 访问 Qt 官网下载页面:https://www.qt.io/offline-installers
    ⚝ 找到 "Qt Online Installers" 部分,选择 "Download the Qt Online Installer for macOS"
    ⚝ 下载 Qt Online Installer 在线安装程序 (例如 qt-unified-mac-x64-online.dmg)。

    运行 Qt Online Installer 安装程序:

    ⚝ 双击打开下载的 DMG 镜像文件 qt-unified-mac-x64-online.dmg
    ⚝ 双击 DMG 镜像中的 Qt Online Installer 图标,运行安装程序。
    ⚝ 按照安装向导的提示进行操作。

    登录 Qt 账号 (可选,但推荐):

    ⚝ 安装程序启动后,会提示登录 Qt 账号。
    ⚝ 如果没有 Qt 账号,可以免费注册一个。
    ⚝ 登录 Qt 账号可以享受 Qt 的完整功能和许可,推荐登录。

    选择 Qt 版本和组件:

    ⚝ 在 "Select Components" 页面,选择要安装的 Qt 版本和组件。
    Qt 版本: 建议选择最新的 LTS 版本,例如 Qt 6.x LTS。
    组件:
    ▮▮▮▮⚝ Qt Modules (Qt 模块): 根据需要选择 Qt 模块,通常需要选择:
    ▮▮▮▮▮▮▮▮⚝ Qt Core (QtCore)
    ▮▮▮▮▮▮▮▮⚝ Qt GUI (QtGui)
    ▮▮▮▮▮▮▮▮⚝ Qt Widgets (QtWidgets)
    ▮▮▮▮▮▮▮▮⚝ Qt Network (QtNetwork)
    ▮▮▮▮▮▮▮▮⚝ Qt SQL (QtSql)
    ▮▮▮▮▮▮▮▮⚝ Qt Multimedia (QtMultimedia)
    ▮▮▮▮▮▮▮▮⚝ Qt Quick (QtQuick)
    ▮▮▮▮▮▮▮▮⚝ Qt WebEngine (QtWebEngine)
    ▮▮▮▮▮▮▮▮⚝ ... (根据项目需求选择其他模块)
    ▮▮▮▮⚝ Qt Tools (Qt 工具): 必须选择:
    ▮▮▮▮▮▮▮▮⚝ Qt Creator (Qt Creator IDE)
    ▮▮▮▮▮▮▮▮⚝ macOS: 选择 macOS 构建工具链,通常默认选择 macOS 即可,使用 Clang 编译器。
    ▮▮▮▮⚝ Examples (示例): 推荐选择,包含大量的 Qt 示例代码,方便学习。
    ▮▮▮▮⚝ Sources (源码): 可选,如果需要查看 Qt 源码,可以选择安装。

    注意: 选择组件越多,安装时间越长,占用磁盘空间越大。建议根据实际需求选择必要的组件。

    选择安装路径:

    ⚝ 在 "Installation Folder" 页面,选择 Qt SDK 的安装路径。
    ⚝ 建议使用默认安装路径 /Users/[YourUserName]/Qt/opt/Qt

    完成安装:

    ⚝ 点击 "Install" 按钮,开始安装 Qt SDK。
    ⚝ 安装过程可能需要一段时间,取决于选择的组件和网络速度。
    ⚝ 安装完成后,点击 "Finish" 按钮,完成安装。

    启动 Qt Creator:

    ⚝ 安装完成后,可以在 "应用程序 (Applications)" 文件夹中找到 Qt Creator 图标,启动 Qt Creator IDE。
    ⚝ 首次启动 Qt Creator,可能会提示配置 Kit (套件)。Kit 包含了 Qt 版本、编译器、调试器等信息。Qt Creator 通常会自动检测并配置 Kit。
    ⚝ 可以打开 Qt Creator 的 "Qt Creator" -> "Preferences..." -> "Kits" -> "Kits" 页面,查看和配置 Kit。

    至此,macOS 环境下的 Qt 开发环境搭建完成。可以开始使用 Qt Creator 创建和开发 Qt 应用程序了。

    1.4.3 Linux 环境下的 Qt 安装与配置

    在 Linux 操作系统下搭建 Qt 开发环境,有多种安装方式,包括在线安装、离线安装、包管理器安装等。以下介绍常用的在线安装包管理器安装两种方式:

    方式一:使用 Qt Online Installer (在线安装)

    下载 Qt SDK:

    ⚝ 访问 Qt 官网下载页面:https://www.qt.io/offline-installers
    ⚝ 找到 "Qt Online Installers" 部分,选择 "Download the Qt Online Installer for Linux"
    ⚝ 下载 Qt Online Installer 在线安装程序 (例如 qt-unified-linux-x64-online.run)。

    运行 Qt Online Installer 安装程序:

    ⚝ 打开终端 (Terminal)。
    ⚝ 进入下载目录,例如 cd Downloads
    ⚝ 添加执行权限:chmod +x qt-unified-linux-x64-online.run
    ⚝ 运行安装程序:./qt-unified-linux-x64-online.run (可能需要使用 sudo 命令,取决于安装路径和权限)。
    ⚝ 按照安装向导的提示进行操作。

    图形界面安装向导:

    ⚝ 安装程序启动后,会弹出图形界面安装向导,与 Windows 和 macOS 平台的安装向导类似。
    登录 Qt 账号 (可选,但推荐): 登录 Qt 账号可以享受 Qt 的完整功能和许可,推荐登录。
    选择 Qt 版本和组件: 与 Windows 和 macOS 平台类似,选择要安装的 Qt 版本和组件。
    ▮▮▮▮⚝ Qt 版本: 建议选择最新的 LTS 版本。
    ▮▮▮▮⚝ Qt Modules: 根据需要选择 Qt 模块,通常需要选择 QtCore, QtGui, QtWidgets, QtNetwork, QtSql, QtMultimedia, QtQuick, QtWebEngine 等。
    ▮▮▮▮⚝ Qt Tools: 必须选择 Qt Creator IDE 和 Linux 构建工具链 (通常默认选择 GCC 编译器)。
    ▮▮▮▮⚝ Examples 和 Sources: 推荐选择。
    选择安装路径: 建议使用默认安装路径 /opt/Qt/home/[YourUserName]/Qt
    完成安装: 点击 "Install" 按钮,等待安装完成。

    启动 Qt Creator:

    ⚝ 安装完成后,可以在应用程序菜单中找到 Qt Creator 快捷方式,或者在终端中输入 qtcreator 命令启动 Qt Creator IDE。
    ⚝ 首次启动 Qt Creator,可能会提示配置 Kit。Qt Creator 通常会自动检测并配置 Kit。
    ⚝ 可以打开 Qt Creator 的 "工具 (Tools)" -> "选项 (Options)" -> "Kit" -> "构建套件 (Kits)" 页面,查看和配置 Kit。

    方式二:使用包管理器安装 (以 Ubuntu/Debian 为例)

    大多数 Linux 发行版都提供了 Qt 的软件包,可以使用包管理器 (例如 apt, yum, pacman, dnf) 方便地安装 Qt SDK 和 Qt Creator。以下以 Ubuntu/Debian 系统为例,使用 apt 包管理器安装 Qt:

    更新软件包列表:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 sudo apt update

    安装 Qt SDK 和 Qt Creator:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 sudo apt install qt6-base-dev qt6-qmake qtcreator

    qt6-base-dev: 安装 Qt 6 核心开发库,包含 QtCore, QtGui, QtWidgets 等模块。
    qt6-qmake: 安装 qmake 构建工具。
    qtcreator: 安装 Qt Creator IDE。

    可以根据需要安装其他 Qt 模块的开发包,例如:

    qt6-multimedia-dev: Qt Multimedia 模块开发包。
    qt6-network-dev: Qt Network 模块开发包。
    qt6-sql-dev: Qt SQL 模块开发包。
    qt6-webengine-dev: Qt WebEngine 模块开发包。
    qtdeclarative6-dev: Qt Quick 和 QML 模块开发包 (Qt 6 版本模块名有所变化)。

    启动 Qt Creator:

    ⚝ 安装完成后,可以在应用程序菜单中找到 Qt Creator 快捷方式,或者在终端中输入 qtcreator 命令启动 Qt Creator IDE。
    ⚝ 包管理器安装的 Qt 通常已经配置好了 Kit,可以直接使用。

    不同 Linux 发行版的安装命令可能略有不同,请根据具体的发行版和包管理器进行调整。

    Linux 发行版常用 Qt 包名 (参考)

    Ubuntu/Debian (apt):
    ▮▮▮▮⚝ qt6-base-dev, qt6-qmake, qtcreator, qt6-multimedia-dev, qt6-network-dev, qt6-sql-dev, qt6-webengine-dev, qtdeclarative6-dev
    Fedora/CentOS/RHEL (dnf/yum):
    ▮▮▮▮⚝ qt6-qtbase-devel, qt6-qttools-qmake, qtcreator, qt6-qtmultimedia-devel, qt6-qtnetwork-devel, qt6-qtsql-devel, qt6-qtwebengine-devel, qt6-qtdeclarative-devel
    Arch Linux (pacman):
    ▮▮▮▮⚝ qt6-base, qt6-tools, qtcreator, qt6-multimedia, qt6-network, qt6-database, qt6-webengine, qt6-declarative
    openSUSE (zypper):
    ▮▮▮▮⚝ libQt6Core-devel, libQt6Gui-devel, libQt6Widgets-devel, qt6-qmake, qt-creator, libQt6Multimedia-devel, libQt6Network-devel, libQt6Sql-devel, libQt6WebEngine-devel, libQt6Declarative-devel

    Linux 环境下的 Qt 开发环境搭建完成。选择合适的安装方式,根据 Linux 发行版和个人习惯进行配置,即可开始 Qt GUI 编程之旅。

    2. Qt 基础:核心概念与架构

    2.1 Qt 元对象系统 (Meta-Object System)

    2.1.1 元对象系统的组成部分:QObject, MOC (元对象编译器), 反射 (Reflection)

    Qt 框架的核心在于其元对象系统 (Meta-Object System),这是一个为 C++ 扩展了反射 (Reflection)信号与槽 (Signals and Slots) 以及属性系统 (Property System) 等动态特性的系统。理解元对象系统是深入 Qt 编程的关键。它主要由以下几个核心部分组成:

    QObject 类
    QObject 是 Qt 对象模型的基石,是所有支持元对象特性的 Qt 类的祖先类 (Ancestor Class)。 任何希望利用 Qt 元对象系统特性的类,都必须直接或间接地继承自 QObjectQObject 类本身提供了诸如对象命名、父子对象关系管理、信号与槽机制的支持、以及属性系统接口等基础功能。

    ▮▮▮▮⚝ 核心作用:
    ▮▮▮▮▮▮▮▮⚝ 作为所有 Qt 元对象类的基类。
    ▮▮▮▮▮▮▮▮⚝ 提供信号与槽 (Signals and Slots)机制的基础。
    ▮▮▮▮▮▮▮▮⚝ 支持属性系统 (Property System)
    ▮▮▮▮▮▮▮▮⚝ 实现对象树 (Object Tree) 的管理。
    ▮▮▮▮▮▮▮▮⚝ 支持动态属性 (Dynamic Properties)

    ▮▮▮▮⚝ 继承自 QObject 的意义:
    ▮▮▮▮▮▮▮▮⚝ 使类具备了 Qt 元对象系统的所有特性。
    ▮▮▮▮▮▮▮▮⚝ 可以使用 Qt 的信号与槽 (Signals and Slots)机制进行对象间通信。
    ▮▮▮▮▮▮▮▮⚝ 可以利用 Qt 的属性系统 (Property System)进行属性的访问和操作。
    ▮▮▮▮▮▮▮▮⚝ 可以被纳入 Qt 的对象树 (Object Tree)管理,实现自动化的内存管理。

    MOC (元对象编译器)
    MOC (Meta-Object Compiler) 是 Qt 工具链中一个非常重要的预处理器。它负责解析 C++ 源代码,特别是那些包含了 Q_OBJECT 宏的头文件。Q_OBJECT 宏是启用元对象特性的关键标记。MOC 会扫描这些类声明,并为它们生成实现元对象特性所需的 C++ 代码。这些生成的代码包括:

    ▮▮▮▮⚝ 元对象代码: 包含了类的元数据 (Metadata),如类名、信号 (Signals)槽 (Slots)属性 (Properties) 等信息,这些元数据使得 Qt 能够在运行时 (Run-time) 对对象进行自省 (Introspection) 和动态操作。
    ▮▮▮▮⚝ 信号与槽 (Signals and Slots)机制的实现代码: MOC 会为 signals 关键字声明的信号 (Signals)slots 关键字声明的槽 (Slots) 生成相应的代码,使得信号能够被发射 (emit),槽能够被连接 (connect) 和调用。
    ▮▮▮▮⚝ 属性系统 (Property System) 的实现代码: MOC 会为 Q_PROPERTY 宏声明的属性 (Properties) 生成读取器 (Getter)写入器 (Setter)通知信号 (Notify Signal) 的相关代码,使得属性可以被动态访问和修改。

    ▮▮▮▮⚝ MOC 的工作流程:
    1. 输入: C++ 源代码文件(特别是头文件)。
    2. 解析: 扫描源代码,查找包含 Q_OBJECT 宏的类声明。
    3. 元数据提取: 提取类中的信号 (Signals)槽 (Slots)属性 (Properties) 等元数据信息。
    4. 代码生成: 生成包含元对象代码的 C++ 源文件(通常是 moc_xxx.cpp 文件)。
    5. 输出: 生成的 C++ 源文件,需要被编译并链接到最终的可执行程序中。

    反射 (Reflection)
    反射 (Reflection) 是一种程序在运行时 (Run-time) 检查自身结构的能力,包括类名、属性 (Properties)、方法(在 Qt 中主要是信号 (Signals)槽 (Slots))等。在传统的 C++ 中,反射支持非常有限。而 Qt 通过 元对象系统 (Meta-Object System) 实现了强大的反射能力。

    ▮▮▮▮⚝ Qt 反射的实现:
    ▮▮▮▮▮▮▮▮⚝ 元数据 (Metadata): MOC 生成的元对象代码包含了类的元数据,这些元数据在运行时被 Qt 框架使用。
    ▮▮▮▮▮▮▮▮⚝ QMetaObject 类: 每个 QObject 类都有一个关联的 QMetaObject 对象,可以通过 QObject::metaObject() 方法获取。QMetaObject 类提供了访问类元数据的方法,例如 className()methodCount()propertyCount() 等。
    ▮▮▮▮▮▮▮▮⚝ 运行时类型信息 (RTTI): 虽然 C++ 本身也提供了 RTTI,但 Qt 的反射机制更加强大和易用,它建立在元对象系统之上,提供了更丰富的运行时类型信息和动态操作能力。

    ▮▮▮▮⚝ 反射的应用:
    ▮▮▮▮▮▮▮▮⚝ 信号与槽 (Signals and Slots)机制: Qt 使用反射来在运行时连接信号 (Signals)槽 (Slots),并进行类型检查。
    ▮▮▮▮▮▮▮▮⚝ 属性系统 (Property System): Qt 使用反射来实现属性的动态访问、修改和通知。
    ▮▮▮▮▮▮▮▮⚝ 对象序列化 (Object Serialization): Qt 的对象序列化机制可以使用反射来保存和加载对象的状态。
    ▮▮▮▮▮▮▮▮⚝ Qt Creator IDE: Qt Creator 等 IDE 工具使用反射来实现代码自动完成、属性查看器等功能。

    总结来说,元对象系统 (Meta-Object System) 是 Qt 框架的灵魂,它通过 QObject 基类、MOC 预处理器和反射机制,为 C++ 带来了动态语言的灵活性和强大的运行时特性,这使得 Qt 能够实现信号与槽 (Signals and Slots)属性系统 (Property System) 等核心功能,并构建出灵活、可扩展的 GUI 应用程序。

    2.1.2 元对象系统的作用:信号与槽、属性系统、动态属性

    元对象系统 (Meta-Object System) 在 Qt 框架中扮演着至关重要的角色,它为 Qt 提供了许多核心功能,使得 Qt 应用程序具有高度的灵活性和可扩展性。元对象系统主要在以下几个方面发挥作用:

    信号与槽 (Signals and Slots)
    信号与槽 (Signals and Slots) 是 Qt 框架中用于对象间通信的核心机制,也是 Qt 区别于其他 GUI 框架的重要特征之一。元对象系统 (Meta-Object System)信号与槽 (Signals and Slots) 机制的基础。

    ▮▮▮▮⚝ 作用:
    ▮▮▮▮▮▮▮▮⚝ 对象间解耦 (Decoupling)信号 (Signals) 发射者无需知道哪个对象或哪些槽 (Slots) 连接到它的信号,实现了对象之间的松耦合 (Loose Coupling)
    ▮▮▮▮▮▮▮▮⚝ 类型安全 (Type Safety)信号 (Signals)槽 (Slots) 的参数类型在编译时 (Compile-time) 确定,MOC 会进行类型检查,确保连接的信号 (Signals)槽 (Slots) 的参数类型兼容,避免了运行时 (Run-time) 的类型错误。
    ▮▮▮▮▮▮▮▮⚝ 灵活性和可扩展性 (Flexibility and Extensibility): 可以在运行时 (Run-time) 动态地建立和断开信号 (Signals)槽 (Slots) 的连接,使得程序具有高度的灵活性和可扩展性。
    ▮▮▮▮▮▮▮▮⚝ 直观易用 (Intuitive and Easy to Use)信号与槽 (Signals and Slots) 的语法简洁明了,易于理解和使用,降低了对象间通信的复杂性。

    ▮▮▮▮⚝ 元对象系统的支持:
    ▮▮▮▮▮▮▮▮⚝ MOC (元对象编译器) 生成了信号 (Signals)槽 (Slots) 的元数据和实现代码。
    ▮▮▮▮▮▮▮▮⚝ QMetaObject 类提供了在运行时 (Run-time) 访问信号 (Signals)槽 (Slots) 元数据的方法,例如 indexOfSignal()indexOfSlot()connect() 等。
    ▮▮▮▮▮▮▮▮⚝ QObject::connect() 方法使用元对象系统在运行时 (Run-time) 建立信号 (Signals)槽 (Slots) 的连接。

    属性系统 (Property System)
    Qt 的属性系统 (Property System) 提供了一种统一 (Unified)动态 (Dynamic) 的方式来访问和操作对象的属性 (Properties)元对象系统 (Meta-Object System)属性系统 (Property System) 的基础。

    ▮▮▮▮⚝ 作用:
    ▮▮▮▮▮▮▮▮⚝ 统一的属性访问接口 (Unified Property Access Interface): 通过 属性系统 (Property System),可以使用统一的 API(例如 setProperty()property())来访问和操作对象的属性 (Properties),而无需关心属性的具体实现方式。
    ▮▮▮▮▮▮▮▮⚝ 属性的元数据 (Property Metadata)属性系统 (Property System) 允许为属性 (Properties) 定义元数据,例如属性名、类型、读取器 (Getter)写入器 (Setter)通知信号 (Notify Signal) 等信息,这些元数据在运行时 (Run-time) 可用。
    ▮▮▮▮▮▮▮▮⚝ 属性的动态特性 (Dynamic Property Features): 可以在运行时 (Run-time) 动态地查询、设置和获取属性 (Properties) 的值,以及连接属性 (Properties)通知信号 (Notify Signal),实现属性绑定 (Property Binding)属性动画 (Property Animation) 等高级功能。
    ▮▮▮▮▮▮▮▮⚝ 与 Qt Designer 和 QML 集成 (Integration with Qt Designer and QML)属性系统 (Property System) 与 Qt Designer 和 QML 等工具紧密集成,使得在可视化设计工具和声明式语言中操作和绑定属性 (Properties) 变得非常方便。

    ▮▮▮▮⚝ 元对象系统的支持:
    ▮▮▮▮▮▮▮▮⚝ MOC (元对象编译器) 生成了属性 (Properties) 的元数据和实现代码,包括读取器 (Getter)写入器 (Setter)通知信号 (Notify Signal) 的代码。
    ▮▮▮▮▮▮▮▮⚝ QMetaObject 类提供了在运行时 (Run-time) 访问属性 (Properties) 元数据的方法,例如 propertyCount()property()userProperty() 等。
    ▮▮▮▮▮▮▮▮⚝ QObject::setProperty()QObject::property() 方法使用元对象系统在运行时 (Run-time) 动态地设置和获取属性 (Properties) 的值。

    动态属性 (Dynamic Properties)
    动态属性 (Dynamic Properties) 是 Qt 属性系统 (Property System) 的一个扩展特性,它允许在运行时 (Run-time) 为 QObject 对象动态地添加新的属性 (Properties)

    ▮▮▮▮⚝ 作用:
    ▮▮▮▮▮▮▮▮⚝ 运行时扩展对象属性 (Run-time Extension of Object Properties): 可以在运行时 (Run-time) 为对象添加新的属性 (Properties),而无需在编译时 (Compile-time) 预先定义。
    ▮▮▮▮▮▮▮▮⚝ 存储和关联运行时数据 (Storing and Associating Run-time Data): 可以使用动态属性来存储和关联与对象相关的运行时数据,例如用户自定义的数据、状态信息等。
    ▮▮▮▮▮▮▮▮⚝ 灵活性和可配置性 (Flexibility and Configurability): 动态属性增加了对象的灵活性和可配置性,使得对象可以适应不同的运行时 (Run-time) 需求。

    ▮▮▮▮⚝ 元对象系统的支持:
    ▮▮▮▮▮▮▮▮⚝ QObject::setProperty() 方法可以用于设置动态属性,如果指定的属性 (Property) 不存在,则会动态地添加一个新的属性 (Property)
    ▮▮▮▮▮▮▮▮⚝ QObject::property() 方法可以用于获取动态属性的值。
    ▮▮▮▮▮▮▮▮⚝ 动态属性的元数据也由 元对象系统 (Meta-Object System) 管理,可以像静态属性一样被访问和操作。

    总而言之,元对象系统 (Meta-Object System) 是 Qt 框架的基石,它通过 信号与槽 (Signals and Slots)属性系统 (Property System)动态属性 (Dynamic Properties) 等机制,为 Qt 应用程序提供了强大的对象间通信、属性管理和运行时动态特性,使得 Qt 成为一个非常灵活、可扩展和高效的 GUI 框架。理解和掌握 元对象系统 (Meta-Object System) 是深入学习 Qt 编程的关键。

    2.2 信号与槽 (Signals and Slots) 机制

    2.2.1 信号与槽的基本概念:信号的发射与槽的连接

    信号与槽 (Signals and Slots) 是 Qt 框架中用于实现对象间通信的核心机制。它是一种类型安全 (Type-safe)松耦合 (Loosely Coupled) 的通信方式,也是 Qt 区别于其他 GUI 框架的重要特征之一。理解 信号与槽 (Signals and Slots) 的基本概念是学习 Qt 编程的首要任务 (Primary Task)

    信号 (Signals)

    ▮▮▮▮⚝ 定义: 信号 (Signals) 是对象在状态 (State) 发生改变或事件 (Event) 发生时发出的通知 (Notification)。例如,一个按钮被点击时,会发射一个 clicked() 信号;一个文本框的文本内容发生改变时,会发射一个 textChanged() 信号。
    ▮▮▮▮⚝ 声明: 信号 (Signals) 在类声明中使用 signals 关键字进行声明。信号 (Signals) 是一种特殊的函数 (Function),但它们没有具体的实现代码,只有函数签名 (函数原型)。
    ▮▮▮▮⚝ 发射 (Emitting)信号 (Signals) 通过 emit 关键字进行发射。当一个信号 (Signal) 被发射时,所有连接到该信号的槽 (Slots) 都会被自动调用。
    ▮▮▮▮⚝ 特点:
    ▮▮▮▮▮▮▮▮⚝ 访问权限 (Access Permission)信号 (Signals) 通常声明为 protectedpublic,但只能由信号 (Signals) 所在类及其子类发射。
    ▮▮▮▮▮▮▮▮⚝ 返回值 (Return Value)信号 (Signals) 通常没有返回值(void 类型)。
    ▮▮▮▮▮▮▮▮⚝ 参数 (Parameters)信号 (Signals) 可以携带参数 (Parameters),用于传递事件或状态改变的相关信息。
    ▮▮▮▮▮▮▮▮⚝ 多对多 (Many-to-Many): 一个信号 (Signal) 可以连接到多个槽 (Slots),一个槽 (Slot) 也可以连接到多个信号 (Signals)
    ▮▮▮▮▮▮▮▮⚝ 异步 (Asynchronous)同步 (Synchronous)信号 (Signals) 的发射可以是异步 (Asynchronous) 的(队列连接 (Queued Connection))或 同步 (Synchronous) 的(直接连接 (Direct Connection)),取决于连接类型。

    ▮▮▮▮⚝ 示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyButton : public QPushButton {
    2 Q_OBJECT
    3 signals:
    4 void doubleClicked(); // 声明一个名为 doubleClicked 的信号,不带参数
    5 void valueChanged(int newValue); // 声明一个名为 valueChanged 的信号,带一个 int 参数
    6 public:
    7 MyButton(const QString &text, QWidget *parent = nullptr) : QPushButton(text, parent) {}
    8 protected:
    9 void mouseDoubleClickEvent(QMouseEvent *event) override {
    10 emit doubleClicked(); // 发射 doubleClicked 信号
    11 QPushButton::mouseDoubleClickEvent(event);
    12 }
    13 void setValue(int value) {
    14 if (m_value != value) {
    15 m_value = value;
    16 emit valueChanged(m_value); // 发射 valueChanged 信号,并传递参数
    17 }
    18 }
    19 private:
    20 int m_value = 0;
    21 };

    槽 (Slots)

    ▮▮▮▮⚝ 定义: 槽 (Slots) 是普通的 C++ 函数 (C++ Functions),用于响应 (Respond to) 接收到的信号 (Signals)。当一个槽 (Slot) 连接到一个信号 (Signal) 后,每当该信号 (Signal) 被发射时,槽 (Slot) 函数就会被自动调用。
    ▮▮▮▮⚝ 声明: 槽 (Slots) 在类声明中使用 slots 关键字进行声明。槽 (Slots) 可以是普通的成员函数,可以是 publicprotectedprivate 访问权限。
    ▮▮▮▮⚝ 连接 (Connecting)信号 (Signals)槽 (Slots) 之间需要通过 QObject::connect() 函数建立连接。连接后,当信号 (Signal) 发射时,与之连接的槽 (Slot) 就会被调用。
    ▮▮▮▮⚝ 特点:
    ▮▮▮▮▮▮▮▮⚝ 普通 C++ 函数 (Normal C++ Functions)槽 (Slots) 就是普通的 C++ 成员函数,可以包含任意的 C++ 代码。
    ▮▮▮▮▮▮▮▮⚝ 访问权限 (Access Permission)槽 (Slots) 可以是 publicprotectedprivate,但通常为了能够被外部连接,会声明为 publicprotected
    ▮▮▮▮▮▮▮▮⚝ 返回值 (Return Value)槽 (Slots) 可以有返回值,也可以没有返回值(void 类型),但槽 (Slots) 的返回值通常会被忽略,信号与槽机制不依赖于槽 (Slots) 的返回值。
    ▮▮▮▮▮▮▮▮⚝ 参数 (Parameters)槽 (Slots) 的参数类型和数量必须与所连接的信号 (Signal) 兼容(参数类型一致或可以隐式转换)。

    ▮▮▮▮⚝ 示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyWidget : public QWidget {
    2 Q_OBJECT
    3 public slots:
    4 void handleButtonClicked() { // 声明一个名为 handleButtonClicked 的槽,不带参数
    5 qDebug() << "Button clicked!";
    6 }
    7 void handleValueChanged(int value) { // 声明一个名为 handleValueChanged 的槽,带一个 int 参数
    8 qDebug() << "Value changed to:" << value;
    9 }
    10 public:
    11 MyWidget(QWidget *parent = nullptr) : QWidget(parent) {
    12 MyButton *button = new MyButton("Click me", this);
    13 connect(button, &MyButton::doubleClicked, this, &MyWidget::handleButtonClicked); // 连接 button 的 doubleClicked 信号到 handleButtonClicked 槽
    14 connect(button, &MyButton::valueChanged, this, &MyWidget::handleValueChanged); // 连接 button 的 valueChanged 信号到 handleValueChanged 槽
    15
    16 QVBoxLayout *layout = new QVBoxLayout(this);
    17 layout->addWidget(button);
    18 }
    19 };

    连接 (Connection)

    ▮▮▮▮⚝ QObject::connect() 函数: 信号 (Signals)槽 (Slots) 之间的连接通过 QObject::connect() 静态函数建立。
    ▮▮▮▮⚝ 函数签名 (Function Signature)QObject::connect() 函数的函数签名 (Function Signature) 非常灵活,支持多种连接方式,包括:
    ▮▮▮▮▮▮▮▮⚝ 函数指针 (Function Pointer) 连接: 使用信号 (Signals)槽 (Slots) 的函数指针进行连接,这是类型安全 (Type-safe) 的连接方式,也是 Qt 5 (Qt 5) 及以后版本推荐的方式。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(senderObject, &SenderClass::signalName, receiverObject, &ReceiverClass::slotName);

    ▮▮▮▮▮▮▮▮⚝ 函数名字符串 (Function Name String) 连接 (不推荐,容易出错且类型不安全): 使用信号 (Signals)槽 (Slots) 的函数名字符串进行连接,这种方式在 Qt 4 (Qt 4) 及以前版本常用,但在 Qt 5 (Qt 5) 及以后版本中不推荐使用,因为它类型不安全 (Type-unsafe),且在重构代码时容易出错。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(senderObject, SIGNAL(signalName(parameterTypes)), receiverObject, SLOT(slotName(parameterTypes))); // Qt 4 风格,不推荐

    ▮▮▮▮⚝ 连接类型 (Connection Type)QObject::connect() 函数的最后一个可选参数用于指定连接类型,常见的连接类型包括:
    ▮▮▮▮▮▮▮▮⚝ Qt::DirectConnection (直接连接): 槽 (Slot) 函数在信号 (Signal) 发射的线程 (Thread) 中被同步 (Synchronously) 调用。这是默认的连接类型。
    ▮▮▮▮▮▮▮▮⚝ Qt::QueuedConnection (队列连接): 槽 (Slot) 函数会被放入接收对象 (Receiver Object)事件队列 (Event Queue) 中,在接收对象 (Receiver Object) 所在的线程 (Thread)事件循环 (Event Loop) 中被异步 (Asynchronously) 调用。
    ▮▮▮▮▮▮▮▮⚝ Qt::AutoConnection (自动连接): Qt 自动根据信号 (Signal) 发射者和槽 (Slot) 接收者是否在同一个线程 (Thread) 中,选择 直接连接 (Direct Connection)队列连接 (Queued Connection)
    ▮▮▮▮▮▮▮▮⚝ Qt::BlockingQueuedConnection (阻塞队列连接): 类似于 队列连接 (Queued Connection),但 信号 (Signal) 发射线程会阻塞,直到 槽 (Slot) 函数执行完毕。通常不建议使用,容易造成界面卡顿。
    ▮▮▮▮▮▮▮▮⚝ Qt::UniqueConnection (唯一连接): 防止同一个 信号 (Signal)槽 (Slot) 之间建立重复的连接。

    ▮▮▮▮⚝ 断开连接 (Disconnecting): 可以使用 QObject::disconnect() 函数断开 信号 (Signals)槽 (Slots) 之间的连接。

    基本概念总结:

    ▮▮▮▮⚝ 信号 (Signals) 是对象发出的通知 (Notifications)
    ▮▮▮▮⚝ 槽 (Slots) 是用于响应 (Respond to) 信号 (Signals)函数 (Functions)
    ▮▮▮▮⚝ 通过 QObject::connect() 函数将 信号 (Signals)槽 (Slots) 连接起来。
    ▮▮▮▮⚝ 当 信号 (Signal) 发射时,与之连接的 槽 (Slots) 会被自动调用。
    ▮▮▮▮⚝ 信号与槽 (Signals and Slots) 实现了对象之间的 松耦合 (Loose Coupling)类型安全 (Type Safety) 的通信。

    理解 信号 (Signals) 的发射和 槽 (Slots) 的连接是掌握 Qt 信号与槽 (Signals and Slots) 机制的基础,也是进行 Qt GUI 编程的关键。

    2.2.2 信号与槽的优势:类型安全、松耦合

    信号与槽 (Signals and Slots) 机制作为 Qt 框架的核心特性,相比于传统的回调函数 (Callback Functions) 等通信方式,具有显著的优势,主要体现在 类型安全 (Type Safety)松耦合 (Loose Coupling) 两个方面。

    类型安全 (Type Safety)

    ▮▮▮▮⚝ 编译时类型检查 (Compile-time Type Checking)信号 (Signals)槽 (Slots) 的参数类型在编译时 (Compile-time) 确定。MOC (元对象编译器) 会在编译期间对 信号 (Signals)槽 (Slots) 的连接进行类型检查,确保连接的 信号 (Signals)槽 (Slots) 的参数类型兼容(参数类型完全一致或可以隐式转换)。如果类型不兼容,编译器会报错,从而在编译阶段 (Compile Stage) 就避免了类型错误。
    ▮▮▮▮⚝ 避免运行时类型错误 (Avoiding Run-time Type Errors): 由于 类型检查 (Type Checking) 在编译时 (Compile-time) 完成,因此可以避免在运行时 (Run-time) 出现由于类型不匹配导致的错误,提高了程序的健壮性 (Robustness)可靠性 (Reliability)
    ▮▮▮▮⚝ 函数指针连接的类型安全 (Type Safety of Function Pointer Connection): 使用函数指针 (Function Pointer) 连接 信号 (Signals)槽 (Slots) (例如 connect(sender, &SenderClass::signalName, receiver, &ReceiverClass::slotName)) 是 类型安全 (Type-safe) 的。编译器会严格检查函数指针的类型,确保连接的 信号 (Signals)槽 (Slots) 的函数签名 (Function Signature) 完全匹配。
    ▮▮▮▮⚝ 对比回调函数 (Comparison with Callback Functions): 传统的回调函数 (Callback Functions) 通常使用函数指针或 void* 指针传递参数,类型安全性较差,容易在运行时 (Run-time) 出现类型错误,且难以调试。而 信号与槽 (Signals and Slots) 机制通过 编译时类型检查 (Compile-time Type Checking),大大提高了类型安全性。

    松耦合 (Loose Coupling)

    ▮▮▮▮⚝ 发送者和接收者解耦 (Decoupling of Sender and Receiver)信号 (Signals) 的发射者 (Sender) 不需要知道哪个对象或哪些 槽 (Slots) 连接到它的 信号 (Signal),也无需知道 槽 (Slots) 的具体实现细节。信号 (Signals) 发射者只负责在状态改变或事件发生时发射 信号 (Signal),而无需关心 信号 (Signal) 如何被处理。
    ▮▮▮▮⚝ 接收者无需知晓发送者 (Receiver Does Not Need to Know Sender)槽 (Slots) 的实现者 (Receiver) 只需要关注如何处理接收到的 信号 (Signal),而不需要知道是哪个对象发射了 信号 (Signal)槽 (Slots) 的实现逻辑与 信号 (Signal) 的发射者完全独立。
    ▮▮▮▮⚝ 中间层:信号与槽机制 (Intermediate Layer: Signals and Slots Mechanism)信号与槽 (Signals and Slots) 机制充当了 信号 (Signal) 发射者和 槽 (Slot) 接收者之间的中间层。信号 (Signals) 发射者和 槽 (Slots) 接收者通过 信号与槽 (Signals and Slots) 机制进行间接通信,而不是直接依赖。这种间接通信方式降低了对象之间的耦合度。
    ▮▮▮▮⚝ 易于维护和扩展 (Easy to Maintain and Extend): 由于 信号 (Signals) 发射者和 槽 (Slots) 接收者之间是 松耦合 (Loosely Coupled) 的,因此修改或替换 信号 (Signals) 发射者或 槽 (Slots) 接收者的代码,通常不会影响到另一方,降低了代码的维护成本,提高了代码的可扩展性。
    ▮▮▮▮⚝ 运行时动态连接 (Run-time Dynamic Connection)信号 (Signals)槽 (Slots) 的连接可以在运行时 (Run-time) 动态建立和断开,进一步增强了对象之间的 松耦合 (Loose Coupling) 和灵活性。可以在不修改代码的情况下,通过配置或动态逻辑来改变对象之间的通信关系。
    ▮▮▮▮⚝ 对比传统回调 (Comparison with Traditional Callbacks): 传统的回调函数 (Callback Functions) 通常需要发送者持有接收者的函数指针,发送者和接收者之间存在直接的依赖关系,耦合度较高。而 信号与槽 (Signals and Slots) 机制通过 中间层 (Intermediate Layer)运行时动态连接 (Run-time Dynamic Connection),实现了更低的耦合度。

    总结:

    ▮▮▮▮⚝ 类型安全 (Type Safety)信号与槽 (Signals and Slots) 机制通过 编译时类型检查 (Compile-time Type Checking),提高了代码的健壮性 (Robustness)可靠性 (Reliability),避免了 运行时类型错误 (Run-time Type Errors)
    ▮▮▮▮⚝ 松耦合 (Loose Coupling)信号与槽 (Signals and Slots) 机制通过 发送者和接收者解耦 (Decoupling of Sender and Receiver)中间层 (Intermediate Layer)运行时动态连接 (Run-time Dynamic Connection),降低了对象之间的耦合度,提高了代码的 可维护性 (Maintainability)可扩展性 (Extensibility)灵活性 (Flexibility)

    信号与槽 (Signals and Slots) 机制的 类型安全 (Type Safety)松耦合 (Loose Coupling) 优势,使得 Qt 框架能够构建出更加 健壮 (Robust)可靠 (Reliable)易于维护 (Easy to Maintain)易于扩展 (Easy to Extend) 的 GUI 应用程序。这也是 Qt 框架在众多 GUI 框架中脱颖而出的重要原因之一。

    2.2.3 信号与槽的连接方式:直接连接、队列连接、自动连接

    QObject::connect() 函数提供了多种连接类型 (Connection Type),用于控制 信号 (Signals) 发射时 槽 (Slots) 的调用方式。常见的连接类型包括 直接连接 (Direct Connection)队列连接 (Queued Connection)自动连接 (Auto Connection)。不同的连接类型适用于不同的场景,理解这些连接类型的特点和应用场景,有助于编写高效、稳定的 Qt 应用程序。

    直接连接 (Direct Connection) ( Qt::DirectConnection )

    ▮▮▮▮⚝ 特点:
    ▮▮▮▮▮▮▮▮⚝ 同步调用 (Synchronous Call): 当 信号 (Signal) 被发射时,槽 (Slot) 函数会在信号 (Signal) 发射的线程 (Thread) 中被立即 (Immediately)同步 (Synchronously) 调用。
    ▮▮▮▮▮▮▮▮⚝ 直接函数调用 (Direct Function Call)信号 (Signal) 发射实际上相当于直接函数调用 (Direct Function Call) 槽 (Slot) 函数。槽 (Slot) 函数的执行栈与 信号 (Signal) 发射点的执行栈是同一个。
    ▮▮▮▮▮▮▮▮⚝ 默认连接类型 (Default Connection Type): 如果 QObject::connect() 函数不显式指定连接类型,默认使用 直接连接 (Direct Connection)(或 自动连接 (Auto Connection),最终可能选择 直接连接 (Direct Connection))。
    ▮▮▮▮▮▮▮▮⚝ 效率高 (High Efficiency)直接连接 (Direct Connection) 的调用效率最高,因为它避免了线程切换和事件队列的开销。

    ▮▮▮▮⚝ 应用场景:
    ▮▮▮▮▮▮▮▮⚝ 同一线程内对象间的通信 (Communication Between Objects in the Same Thread): 当 信号 (Signal) 发射者和 槽 (Slot) 接收者在同一个线程 (Thread) 中时,通常使用 直接连接 (Direct Connection),以获得最高的性能。
    ▮▮▮▮▮▮▮▮⚝ 需要立即响应的场景 (Scenarios Requiring Immediate Response): 当 信号 (Signal) 发射后,需要 槽 (Slot) 函数立即执行并处理,且处理过程耗时较短,不会阻塞界面线程时,可以使用 直接连接 (Direct Connection)

    ▮▮▮▮⚝ 注意事项:
    ▮▮▮▮▮▮▮▮⚝ 避免长时间阻塞 (Avoid Long-time Blocking): 由于 直接连接 (Direct Connection)同步调用 (Synchronous Call),如果 槽 (Slot) 函数执行时间过长,会阻塞信号 (Signal) 发射线程,如果 信号 (Signal) 发射线程是 GUI 线程,则可能导致界面卡顿 (UI Freezing)。因此,在 直接连接 (Direct Connection)槽 (Slot) 函数中,应避免执行耗时的操作。
    ▮▮▮▮▮▮▮▮⚝ 线程安全问题 (Thread Safety Issues): 如果 信号 (Signal) 发射者和 槽 (Slot) 接收者不在同一个线程,但仍然使用了 直接连接 (Direct Connection) (通常是不正确的用法),槽 (Slot) 函数会在信号 (Signal) 发射线程中执行,可能导致线程安全问题 (Thread Safety Issues),例如数据竞争 (Data Race)。

    队列连接 (Queued Connection) ( Qt::QueuedConnection )

    ▮▮▮▮⚝ 特点:
    ▮▮▮▮▮▮▮▮⚝ 异步调用 (Asynchronous Call): 当 信号 (Signal) 被发射时,槽 (Slot) 函数不会被立即调用,而是会被放入接收对象 (Receiver Object)事件队列 (Event Queue) 中。槽 (Slot) 函数会在接收对象 (Receiver Object) 所在的线程 (Thread)事件循环 (Event Loop) 中被异步 (Asynchronously) 调用。
    ▮▮▮▮▮▮▮▮⚝ 事件队列机制 (Event Queue Mechanism)队列连接 (Queued Connection) 基于 Qt 的事件队列 (Event Queue) 机制实现。信号 (Signal) 发射相当于向 事件队列 (Event Queue) 中投递一个事件,事件循环 (Event Loop) 会在稍后处理该事件并调用 槽 (Slot) 函数。
    ▮▮▮▮▮▮▮▮⚝ 线程安全 (Thread-safe)队列连接 (Queued Connection)线程安全 (Thread-safe) 的。即使 信号 (Signal) 发射者和 槽 (Slot) 接收者不在同一个线程,槽 (Slot) 函数也会在 接收对象 (Receiver Object) 所在的线程中安全地执行。
    ▮▮▮▮▮▮▮▮⚝ 性能开销 (Performance Overhead)队列连接 (Queued Connection) 的性能开销比 直接连接 (Direct Connection) 略高,因为它涉及到线程切换、事件队列操作等。

    ▮▮▮▮⚝ 应用场景:
    ▮▮▮▮▮▮▮▮⚝ 跨线程对象间的通信 (Communication Between Objects in Different Threads): 当 信号 (Signal) 发射者和 槽 (Slot) 接收者不在同一个线程 (Thread) 中时,必须使用 队列连接 (Queued Connection) (或 自动连接 (Auto Connection),最终可能选择 队列连接 (Queued Connection)),以确保线程安全 (Thread Safety)
    ▮▮▮▮▮▮▮▮⚝ 避免阻塞 GUI 线程 (Avoiding Blocking GUI Thread): 当 槽 (Slot) 函数执行时间较长,可能会阻塞界面线程时,可以使用 队列连接 (Queued Connection),将耗时操作放在工作线程 (Worker Thread) 中执行,避免界面卡顿 (UI Freezing)。

    ▮▮▮▮⚝ 注意事项:
    ▮▮▮▮▮▮▮▮⚝ 异步特性 (Asynchronous Feature)队列连接 (Queued Connection)异步 (Asynchronous) 的,信号 (Signal) 发射后,槽 (Slot) 函数不会立即执行,而是在稍后被调用。因此,信号 (Signal) 发射后的状态改变不会立即反映在 槽 (Slot) 函数中。
    ▮▮▮▮▮▮▮▮⚝ 事件循环 (Event Loop) 依赖: 队列连接 (Queued Connection) 依赖于接收对象 (Receiver Object) 所在线程的事件循环 (Event Loop) 正常运行。如果接收对象 (Receiver Object) 所在线程没有事件循环 (Event Loop),或者 事件循环 (Event Loop) 被阻塞,队列连接 (Queued Connection) 将无法正常工作。

    自动连接 (Auto Connection) ( Qt::AutoConnection )

    ▮▮▮▮⚝ 特点:
    ▮▮▮▮▮▮▮▮⚝ 自动选择连接类型 (Automatic Connection Type Selection)Qt::AutoConnectionQObject::connect() 函数的默认连接类型(如果未显式指定连接类型)。Qt 会自动检测 信号 (Signal) 发射者和 槽 (Slot) 接收者是否在同一个线程 (Thread) 中,并根据检测结果自动选择 直接连接 (Direct Connection)队列连接 (Queued Connection)
    ▮▮▮▮▮▮▮▮⚝ 同一线程:直接连接 (Same Thread: Direct Connection): 如果 信号 (Signal) 发射者和 槽 (Slot) 接收者在同一个线程 (Thread) 中,Qt::AutoConnection 会选择 直接连接 (Direct Connection)
    ▮▮▮▮▮▮▮▮⚝ 不同线程:队列连接 (Different Threads: Queued Connection): 如果 信号 (Signal) 发射者和 槽 (Slot) 接收者不在同一个线程 (Thread) 中,Qt::AutoConnection 会选择 队列连接 (Queued Connection)
    ▮▮▮▮▮▮▮▮⚝ 方便易用 (Convenient and Easy to Use)自动连接 (Auto Connection) 简化了连接类型的选择,开发者无需显式指定连接类型,Qt 会自动选择合适的连接方式。

    ▮▮▮▮⚝ 应用场景:
    ▮▮▮▮▮▮▮▮⚝ 通用场景 (General Scenarios): 在大多数情况下,可以使用 自动连接 (Auto Connection),让 Qt 自动选择合适的连接类型。
    ▮▮▮▮▮▮▮▮⚝ 不确定线程关系 (Uncertain Thread Relationship): 当不确定 信号 (Signal) 发射者和 槽 (Slot) 接收者是否会在同一个线程 (Thread) 中运行时,可以使用 自动连接 (Auto Connection),确保 线程安全 (Thread Safety) 和性能兼顾。

    ▮▮▮▮⚝ 注意事项:
    ▮▮▮▮▮▮▮▮⚝ 自动选择的局限性 (Limitations of Automatic Selection)自动连接 (Auto Connection) 的选择是基于 信号 (Signal) 发射时 信号 (Signal) 发射者和 槽 (Slot) 接收者的线程关系来判断的。如果线程关系在 信号 (Signal) 发射前发生变化,自动连接 (Auto Connection) 的选择可能不是最优的。
    ▮▮▮▮▮▮▮▮⚝ 性能考虑 (Performance Consideration): 在性能敏感的场景下,如果明确知道 信号 (Signal) 发射者和 槽 (Slot) 接收者在同一个线程,显式使用 直接连接 (Direct Connection) 可能比 自动连接 (Auto Connection) 略微高效。

    其他连接类型:

    ▮▮▮▮⚝ Qt::BlockingQueuedConnection (阻塞队列连接): 类似于 队列连接 (Queued Connection),但 信号 (Signal) 发射线程会阻塞,直到 槽 (Slot) 函数执行完毕。通常不建议使用,容易造成界面卡顿。
    ▮▮▮▮⚝ Qt::UniqueConnection (唯一连接): 防止同一个 信号 (Signal)槽 (Slot) 之间建立重复的连接。如果连接已经存在,则 QObject::connect() 函数返回 false,连接不会被重复建立。

    连接类型选择建议:

    ▮▮▮▮⚝ 同一线程内通信: 优先使用 直接连接 (Direct Connection),以获得最高性能。
    ▮▮▮▮⚝ 跨线程通信: 必须使用 队列连接 (Queued Connection)自动连接 (Auto Connection),确保 线程安全 (Thread Safety)
    ▮▮▮▮⚝ 不确定线程关系: 使用 自动连接 (Auto Connection),让 Qt 自动选择合适的连接类型。
    ▮▮▮▮⚝ 避免 GUI 线程阻塞: 对于耗时操作,使用 队列连接 (Queued Connection),将操作放在 工作线程 (Worker Thread) 中执行。
    ▮▮▮▮⚝ 慎用阻塞队列连接: Qt::BlockingQueuedConnection 通常不建议使用,容易造成界面卡顿。

    理解和合理选择 信号与槽 (Signals and Slots)连接类型 (Connection Type),是编写高效、稳定、线程安全的 Qt GUI 应用程序的关键技能之一。

    2.2.4 自定义信号与槽

    Qt 的 信号与槽 (Signals and Slots) 机制不仅可以使用 Qt 预定义的 信号 (Signals)槽 (Slots) (例如 Qt 控件自带的 信号 (Signals)槽 (Slots)),也允许开发者自定义 (Customize) 自己的 信号 (Signals)槽 (Slots),以满足特定的对象间通信需求。自定义信号与槽 (Custom Signals and Slots) 是 Qt 编程中非常常用且强大的技术,能够构建灵活、可扩展的应用程序。

    自定义信号 (Custom Signals)

    ▮▮▮▮⚝ 声明位置: 自定义信号 (Custom Signals) 必须在类声明的 signals 关键字下声明。
    ▮▮▮▮⚝ 语法: 自定义信号 (Custom Signals) 的声明语法类似于函数声明,但没有函数体,只有函数签名 (函数原型)。自定义信号 (Custom Signals) 通常没有返回值(void 类型)。
    ▮▮▮▮⚝ 参数: 自定义信号 (Custom Signals) 可以携带零个或多个参数,用于传递事件或状态改变的相关信息。参数类型可以是任何 C++ 类型,但为了保证类型安全,建议使用 Qt 支持的元类型 (Meta-Type) 或可以被 Qt 元系统识别的类型。
    ▮▮▮▮⚝ 发射: 自定义信号 (Custom Signals) 需要在适当的时机使用 emit 关键字进行发射。信号 (Signal) 的发射通常发生在对象的状态发生改变或特定事件发生时。
    ▮▮▮▮⚝ 访问权限: 自定义信号 (Custom Signals) 通常声明为 protectedpublic,但只能由 信号 (Signal) 所在类及其子类发射。外部对象不能直接发射 信号 (Signal)
    ▮▮▮▮⚝ 示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyCustomObject : public QObject {
    2 Q_OBJECT
    3 signals:
    4 void dataReady(const QString &data); // 自定义信号 dataReady,携带 QString 参数
    5 void errorOccurred(int errorCode, const QString &errorMessage); // 自定义信号 errorOccurred,携带 int 和 QString 参数
    6 public:
    7 MyCustomObject(QObject *parent = nullptr) : QObject(parent) {}
    8
    9 void processData() {
    10 // ... 数据处理逻辑 ...
    11 bool success = processDataInternal();
    12 if (success) {
    13 QString resultData = getResultData();
    14 emit dataReady(resultData); // 数据处理成功,发射 dataReady 信号,并传递结果数据
    15 } else {
    16 int errorCode = getErrorCode();
    17 QString errorMessage = getErrorMessage();
    18 emit errorOccurred(errorCode, errorMessage); // 数据处理失败,发射 errorOccurred 信号,并传递错误信息
    19 }
    20 }
    21
    22 private:
    23 bool processDataInternal() { /* ... */ return true; }
    24 QString getResultData() { /* ... */ return "Data processed successfully!"; }
    25 int getErrorCode() { /* ... */ return -1; }
    26 QString getErrorMessage() { /* ... */ return "Unknown error."; }
    27 };

    自定义槽 (Custom Slots)

    ▮▮▮▮⚝ 声明位置: 自定义槽 (Custom Slots) 必须在类声明的 slots 关键字下声明。
    ▮▮▮▮⚝ 语法: 自定义槽 (Custom Slots) 的声明语法与普通 C++ 成员函数相同。自定义槽 (Custom Slots) 可以有返回值,也可以没有返回值(void 类型)。
    ▮▮▮▮⚝ 参数: 自定义槽 (Custom Slots) 的参数类型和数量必须与所连接的 信号 (Signal) 兼容(参数类型一致或可以隐式转换)。
    ▮▮▮▮⚝ 实现: 自定义槽 (Custom Slots) 是普通的 C++ 成员函数,需要提供具体的函数实现代码,用于处理接收到的 信号 (Signals)
    ▮▮▮▮⚝ 访问权限: 自定义槽 (Custom Slots) 可以是 publicprotectedprivate 访问权限。为了能够被外部连接,通常声明为 publicprotected
    ▮▮▮▮⚝ 示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyConsumerObject : public QObject {
    2 Q_OBJECT
    3 public slots:
    4 void handleDataReady(const QString &data) { // 自定义槽 handleDataReady,接收 QString 参数
    5 qDebug() << "Data received:" << data;
    6 // ... 处理接收到的数据 ...
    7 }
    8
    9 void handleDataError(int errorCode, const QString &errorMessage) { // 自定义槽 handleDataError,接收 int 和 QString 参数
    10 qDebug() << "Error occurred:" << errorCode << errorMessage;
    11 // ... 处理错误 ...
    12 }
    13
    14 public:
    15 MyConsumerObject(QObject *parent = nullptr) : QObject(parent) {}
    16 };

    连接自定义信号与槽:

    ▮▮▮▮⚝ 使用 QObject::connect() 函数连接 自定义信号 (Custom Signals)自定义槽 (Custom Slots),连接方式与连接 Qt 预定义的 信号 (Signals)槽 (Slots) 相同,可以使用函数指针或函数名字符串 (不推荐) 进行连接。
    ▮▮▮▮⚝ 示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 MyCustomObject *producer = new MyCustomObject();
    2 MyConsumerObject *consumer = new MyConsumerObject();
    3
    4 connect(producer, &MyCustomObject::dataReady, consumer, &MyConsumerObject::handleDataReady); // 连接 producer 的 dataReady 信号到 consumer 的 handleDataReady 槽
    5 connect(producer, &MyCustomObject::errorOccurred, consumer, &MyConsumerObject::handleDataError); // 连接 producer 的 errorOccurred 信号到 consumer 的 handleDataError 槽
    6
    7 producer->processData(); // 触发 producer 的数据处理,可能会发射 dataReady 或 errorOccurred 信号

    自定义信号与槽的应用场景:

    ▮▮▮▮⚝ 模块间通信 (Inter-module Communication): 在大型应用程序中,可以使用 自定义信号与槽 (Custom Signals and Slots) 实现不同模块之间的通信,降低模块之间的耦合度。
    ▮▮▮▮⚝ 状态通知 (State Notification): 当对象的状态发生改变时,可以通过发射 自定义信号 (Custom Signals) 通知其他对象,例如数据模型 (Data Model) 的数据改变通知视图 (View) 更新界面。
    ▮▮▮▮⚝ 异步事件处理 (Asynchronous Event Handling): 可以使用 自定义信号与槽 (Custom Signals and Slots) 处理异步事件,例如网络请求完成、文件 I/O 完成等事件,通过 信号 (Signals) 通知事件结果,并通过 槽 (Slots) 处理事件结果。
    ▮▮▮▮⚝ 扩展控件功能 (Extending Widget Functionality): 可以自定义控件类,并添加 自定义信号与槽 (Custom Signals and Slots),扩展控件的功能,例如自定义控件的状态改变通知、用户自定义事件响应等。

    自定义信号与槽的优势:

    ▮▮▮▮⚝ 代码复用 (Code Reusability)自定义信号与槽 (Custom Signals and Slots) 可以在不同的类和对象之间复用,提高了代码的复用率。
    ▮▮▮▮⚝ 模块化设计 (Modular Design): 通过 自定义信号与槽 (Custom Signals and Slots),可以将应用程序拆分成独立的模块,模块之间通过 信号 (Signals)槽 (Slots) 进行通信,提高了代码的模块化程度。
    ▮▮▮▮⚝ 灵活性和可扩展性 (Flexibility and Extensibility)自定义信号与槽 (Custom Signals and Slots) 使得对象间的通信更加灵活和可扩展,可以根据需求动态地建立和断开连接,方便应用程序的定制和扩展。

    总结:

    自定义信号与槽 (Custom Signals and Slots) 是 Qt 框架提供的一种强大的对象间通信机制,它允许开发者根据应用程序的具体需求,自定义 (Customize) 自己的 信号 (Signals)槽 (Slots),实现灵活、类型安全、松耦合的对象间通信。掌握 自定义信号与槽 (Custom Signals and Slots) 的使用是深入 Qt 编程的关键技能之一。

    2.3 Qt 对象模型 (Object Model)

    2.3.1 对象树:父子关系与对象生命周期管理

    Qt 的对象模型 (Object Model) 基于 对象树 (Object Tree) 的概念,对象树 (Object Tree) 是 Qt 对象模型的核心组成部分,它提供了一种结构化 (Structured)自动化 (Automated) 的方式来管理 QObject 及其子类的对象的生命周期 (Lifecycle),特别是内存管理。父子关系 (Parent-Child Relationship)对象树 (Object Tree) 的基础,它决定了对象的所有权 (Ownership)生命周期 (Lifecycle)

    对象树 (Object Tree) 的概念:

    ▮▮▮▮⚝ 层次结构 (Hierarchical Structure)对象树 (Object Tree) 是一种树形结构,由 QObject 对象及其子类对象构成。在 对象树 (Object Tree) 中,每个对象都有一个父对象 (Parent Object) (除了根对象),可以有零个或多个子对象 (Child Objects)
    ▮▮▮▮⚝ 父子关系 (Parent-Child Relationship)对象树 (Object Tree) 的核心是 父子关系 (Parent-Child Relationship)。当创建一个 QObject 对象时,可以指定一个 父对象 (Parent Object)。如果指定了 父对象 (Parent Object),则新创建的对象将成为 父对象 (Parent Object)子对象 (Child Object)
    ▮▮▮▮⚝ 根对象 (Root Object)对象树 (Object Tree) 中可以有一个或多个 根对象 (Root Object)根对象 (Root Object) 没有 父对象 (Parent Object),它们通常是应用程序的主窗口 (例如 QMainWindow) 或其他顶层对象。
    ▮▮▮▮⚝ 对象的所有权 (Object Ownership)父对象 (Parent Object) 拥有其 子对象 (Child Objects)所有权 (Ownership)所有权 (Ownership) 意味着 父对象 (Parent Object) 负责 子对象 (Child Objects)内存管理 (Memory Management),包括创建、销毁和内存释放。
    ▮▮▮▮⚝ 对象生命周期管理 (Object Lifecycle Management)对象树 (Object Tree) 提供了一种自动化的对象生命周期管理 (Object Lifecycle Management) 机制。当一个 父对象 (Parent Object) 被销毁时,Qt 框架会自动销毁其所有的 子对象 (Child Objects),以及 子对象 (Child Objects)子对象 (Child Objects),依此类推,形成一个级联删除 (Cascading Deletion) 的过程。

    建立父子关系:

    ▮▮▮▮⚝ 构造函数 (Constructor): 在创建 QObject 对象时,可以通过构造函数的 parent 参数指定 父对象 (Parent Object)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 创建 button 对象,parentWidget 作为其父对象
    2 QPushButton *button = new QPushButton("Click me", parentWidget);

    ▮▮▮▮⚝ setParent() 方法: 可以使用 QObject::setParent() 方法显式地设置对象的 父对象 (Parent Object)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPushButton *button = new QPushButton("Click me");
    2 button->setParent(parentWidget); // 设置 parentWidget 为 button 的父对象

    ▮▮▮▮⚝ 添加子对象 (Adding Child Objects): 当一个对象成为另一个对象的 子对象 (Child Object) 时,Qt 框架会自动将其添加到 父对象 (Parent Object)子对象列表 (Child Object List) 中。可以使用 QObject::children() 方法获取一个对象的 子对象列表 (Child Object List)

    对象生命周期管理:

    ▮▮▮▮⚝ 自动内存回收 (Automatic Memory Reclamation)对象树 (Object Tree) 实现了 自动内存回收 (Automatic Memory Reclamation) 机制。当一个 QObject 对象被销毁时 (例如超出作用域、显式 delete 等),Qt 框架会自动遍历其 子对象列表 (Child Object List),并依次销毁所有的 子对象 (Child Objects)。这个过程是递归 (Recursive) 的,确保整个 对象树 (Object Tree) 中的对象都能被正确地销毁和释放内存,避免内存泄漏 (Memory Leak)。
    ▮▮▮▮⚝ delete 操作: 当使用 delete 操作符销毁一个 QObject 对象时,如果该对象有 父对象 (Parent Object),Qt 框架会自动将其从 父对象 (Parent Object)子对象列表 (Child Object List) 中移除。如果该对象没有 父对象 (Parent Object) (即根对象),则会直接销毁该对象及其所有 子对象 (Child Objects)
    ▮▮▮▮⚝ 父对象销毁时子对象自动销毁 (Child Objects Automatically Destroyed When Parent is Destroyed): 这是 对象树 (Object Tree) 最重要的特性之一。当一个 父对象 (Parent Object) 被销毁时,无论是显式 delete 还是程序退出时自动销毁,其所有的 子对象 (Child Objects) 都会被自动销毁,无需手动 delete 子对象 (Child Objects)。这大大简化了内存管理,降低了内存泄漏的风险。
    ▮▮▮▮⚝ 析构函数 (Destructor)QObject 的析构函数是虚函数 (Virtual Function)。当一个 QObject 对象被销毁时,会先调用其自身的析构函数,然后再递归地销毁其所有的 子对象 (Child Objects)。在自定义的 QObject 子类中,应该在析构函数中释放对象自身占用的资源,例如动态分配的内存、打开的文件句柄等。

    对象树的应用场景:

    ▮▮▮▮⚝ GUI 组件管理 (GUI Component Management): 在 GUI 应用程序中,对象树 (Object Tree) 被广泛用于管理 GUI 组件 (Widgets)。例如,一个窗口 (例如 QMainWindow) 可以作为根对象,窗口中的菜单栏 (Menu Bar)、工具栏 (Tool Bar)、中心部件 (Central Widget) 等都可以作为窗口的 子对象 (Child Objects)。中心部件中又可以包含各种布局 (Layouts) 和控件 (Widgets),形成一个层次清晰的 对象树 (Object Tree)
    ▮▮▮▮⚝ 资源管理 (Resource Management)对象树 (Object Tree) 可以用于管理各种资源对象,例如网络连接 (Network Connection)、数据库连接 (Database Connection)、文件句柄 (File Handle) 等。可以将资源对象设置为某个管理对象的 子对象 (Child Object),当管理对象被销毁时,资源对象也会被自动释放,避免资源泄漏。
    ▮▮▮▮⚝ 模块化设计 (Modular Design): 可以使用 对象树 (Object Tree) 构建模块化的应用程序结构。每个模块可以由一个根对象和一组 子对象 (Child Objects) 组成,模块之间的依赖关系可以通过 对象树 (Object Tree) 来管理。

    对象树的优势:

    ▮▮▮▮⚝ 简化内存管理 (Simplified Memory Management)对象树 (Object Tree) 实现了 自动内存回收 (Automatic Memory Reclamation),开发者无需手动 delete 大部分 QObject 对象,大大简化了内存管理,降低了内存泄漏的风险。
    ▮▮▮▮⚝ 结构化对象组织 (Structured Object Organization)对象树 (Object Tree) 提供了一种结构化的方式来组织和管理 QObject 对象,使得应用程序的对象结构更加清晰、易于理解和维护。
    ▮▮▮▮⚝ 资源自动释放 (Automatic Resource Release): 通过 对象树 (Object Tree),可以实现资源的自动释放,例如在窗口关闭时自动关闭网络连接、数据库连接等资源,提高了应用程序的资源管理效率和可靠性。
    ▮▮▮▮⚝ 避免悬 dangling 指针 (Avoiding Dangling Pointers): 由于 子对象 (Child Objects) 的生命周期由 父对象 (Parent Object) 管理,当 父对象 (Parent Object) 销毁时,子对象 (Child Objects) 也被自动销毁,避免了 悬 dangling 指针 (Dangling Pointers) 的问题。

    总结:

    Qt 的 对象树 (Object Tree) 是一种强大的对象管理机制,它通过 父子关系 (Parent-Child Relationship) 实现了 对象生命周期管理 (Object Lifecycle Management)自动内存回收 (Automatic Memory Reclamation)对象树 (Object Tree) 简化了内存管理,提高了代码的 可靠性 (Reliability)可维护性 (Maintainability)可扩展性 (Extensibility),是 Qt 框架的核心特性之一。在 Qt 编程中,应充分利用 对象树 (Object Tree) 的优势,构建结构清晰、资源管理高效的应用程序。

    2.3.2 Qt 的内存管理:自动内存回收机制

    Qt 的内存管理 (Memory Management) 很大程度上依赖于其 对象树 (Object Tree) 机制,通过 对象树 (Object Tree) 实现了 自动内存回收 (Automatic Garbage Collection) (更准确地说是 Resource Acquisition Is Initialization (RAII)析构函数 (Destructor) 链式调用)。虽然 Qt 的内存管理不是严格意义上的 垃圾回收 (Garbage Collection) 系统 (例如 Java 或 Python 的 GC),但它提供了一种高效、可靠的 自动内存管理 (Automatic Memory Management) 方案,极大地简化了 C++ GUI 应用程序的内存管理工作。

    RAII (Resource Acquisition Is Initialization) 惯用法:

    ▮▮▮▮⚝ 资源获取即初始化 (Resource Acquisition Is Initialization): RAII 是一种 C++ 编程的 惯用法 (Idiom),其核心思想是 资源 (Resources) 的生命周期与对象的生命周期绑定。在对象构造函数 (Constructor) 中获取资源 (例如分配内存、打开文件、获取锁等),在对象析构函数 (Destructor) 中释放资源 (例如释放内存、关闭文件、释放锁等)。
    ▮▮▮▮⚝ Qt 中的 RAII: Qt 框架广泛应用了 RAII 惯用法。例如,QObject 及其子类的对象,内存分配通常在构造函数中完成,内存释放则在析构函数中进行。通过 对象树 (Object Tree)父子关系 (Parent-Child Relationship)析构函数 (Destructor) 链式调用,实现了资源的自动管理。
    ▮▮▮▮⚝ 避免手动 new/delete 大部分 QObject 对象: 在 Qt 编程中,通常不需要手动 new/delete 大部分 QObject 对象,特别是那些作为 子对象 (Child Objects) 添加到 对象树 (Object Tree) 中的对象。这些对象的内存管理由 对象树 (Object Tree) 自动处理。

    析构函数 (Destructor) 链式调用:

    ▮▮▮▮⚝ QObject 的虚析构函数 (Virtual Destructor)QObject 类的析构函数是虚函数 (Virtual Function)。这使得当通过基类指针删除派生类对象时,能够正确调用派生类的析构函数,实现多态析构 (Polymorphic Destruction)。
    ▮▮▮▮⚝ 析构函数链 (Destructor Chain): 当一个 QObject 对象被销毁时,会先调用其自身的析构函数,然后在析构函数中,Qt 框架会自动遍历其 子对象列表 (Child Object List),并依次递归地销毁所有的 子对象 (Child Objects)。这个过程形成一个 析构函数链 (Destructor Chain),确保整个 对象树 (Object Tree) 中的对象都能被正确地销毁。
    ▮▮▮▮⚝ 资源释放 (Resource Release): 在自定义的 QObject 子类的析构函数中,应该释放对象自身占用的资源,例如动态分配的内存、打开的文件句柄、持有的锁等。由于 析构函数 (Destructor) 的链式调用,可以确保在 对象树 (Object Tree) 中的所有对象及其资源都能被正确地释放,避免内存泄漏和资源泄漏。

    自动内存回收的机制:

    ▮▮▮▮⚝ 对象树 (Object Tree) 的父子关系: 父对象 (Parent Object) 拥有 子对象 (Child Objects) 的所有权,负责 子对象 (Child Objects) 的内存管理。
    ▮▮▮▮⚝ 析构函数 (Destructor) 链式调用: 当 父对象 (Parent Object) 被销毁时,会触发 析构函数 (Destructor) 的链式调用,递归地销毁所有的 子对象 (Child Objects)
    ▮▮▮▮⚝ delete 操作符: 使用 delete 操作符删除 QObject 对象时,会触发对象的析构函数,进而触发 析构函数 (Destructor) 链式调用。
    ▮▮▮▮⚝ 程序退出时的自动清理: 当 Qt 应用程序退出时,Qt 框架会自动销毁所有的顶层窗口 (Top-Level Windows) 和根对象 (Root Objects),进而触发整个 对象树 (Object Tree) 的销毁和内存释放。

    内存管理最佳实践:

    ▮▮▮▮⚝ 尽可能使用对象树管理对象: 对于 GUI 组件、资源对象等,尽可能将其设置为某个管理对象的 子对象 (Child Object),利用 对象树 (Object Tree) 进行自动内存管理。
    ▮▮▮▮⚝ 避免手动 new/delete 大部分 QObject 对象: 除非有特殊需求 (例如对象生命周期不受 对象树 (Object Tree) 管理),否则应尽量避免手动 new/delete 大部分 QObject 对象。
    ▮▮▮▮⚝ 在析构函数中释放资源: 在自定义的 QObject 子类的析构函数中,确保释放对象自身占用的所有资源,例如动态分配的内存、打开的文件句柄等。
    ▮▮▮▮⚝ 注意循环引用 (Circular References): 避免在 对象树 (Object Tree) 中创建循环引用 (例如 A 对象是 B 对象的父对象,同时 B 对象又是 A 对象的父对象),循环引用会导致 对象树 (Object Tree) 无法正常销毁,可能造成内存泄漏。
    ▮▮▮▮⚝ 使用智能指针 (Smart Pointers) (谨慎): 虽然 Qt 的 对象树 (Object Tree) 已经提供了强大的内存管理机制,但在某些特殊情况下,可能需要使用 智能指针 (Smart Pointers) (例如 QSharedPointer, QScopedPointer) 来管理非 QObject 对象或需要更精细控制的对象生命周期。但应谨慎使用,避免与 对象树 (Object Tree) 的内存管理机制冲突。

    Qt 内存管理与垃圾回收 (Garbage Collection) 的区别:

    ▮▮▮▮⚝ 非垃圾回收 (Not Garbage Collection): Qt 的内存管理不是严格意义上的 垃圾回收 (Garbage Collection) 系统。垃圾回收 (Garbage Collection) 系统通常在运行时 (Run-time) 自动检测和回收不再使用的内存,而 Qt 的内存管理是基于 对象树 (Object Tree)析构函数 (Destructor) 的确定性 (Deterministic) 内存管理。
    ▮▮▮▮⚝ 确定性内存管理 (Deterministic Memory Management): Qt 的内存管理是 确定性 (Deterministic) 的,对象的销毁时机是确定的,例如当 父对象 (Parent Object) 被销毁时,子对象 (Child Objects) 就会被立即销毁。而 垃圾回收 (Garbage Collection) 系统通常是非确定性 (Non-deterministic) 的,垃圾回收的时机由 垃圾回收器 (Garbage Collector) 决定,开发者无法精确控制。
    ▮▮▮▮⚝ 性能优势 (Performance Advantage): Qt 的 确定性内存管理 (Deterministic Memory Management) 通常具有更高的性能,因为它避免了 垃圾回收 (Garbage Collection) 系统运行时 (Run-time) 的开销 (例如垃圾检测、标记、回收等)。
    ▮▮▮▮⚝ 资源管理更灵活 (More Flexible Resource Management): Qt 的 对象树 (Object Tree) 不仅可以管理内存,还可以管理其他类型的资源 (例如文件句柄、网络连接等)。通过 析构函数 (Destructor),可以方便地实现各种资源的自动释放,资源管理更加灵活和强大。

    总结:

    Qt 的 自动内存回收机制 (Automatic Memory Reclamation Mechanism) 基于 对象树 (Object Tree)析构函数 (Destructor) 链式调用,是一种高效、可靠的 确定性内存管理 (Deterministic Memory Management) 方案。它简化了 C++ GUI 应用程序的内存管理工作,降低了内存泄漏的风险,提高了应用程序的性能和稳定性。在 Qt 编程中,应充分利用 对象树 (Object Tree) 的优势,遵循内存管理最佳实践,构建高质量的 Qt 应用程序。

    2.4 Qt 的模块化设计

    2.4.1 常用 Qt 模块介绍:QtCore, QtGui, QtWidgets, QtNetwork, QtSql 等

    Qt 框架采用了模块化设计 (Modular Design),将功能划分为多个独立的模块 (Modules)。这种模块化设计 (Modular Design) 使得 Qt 框架结构清晰、易于维护、功能扩展灵活,同时也允许开发者根据项目需求选择性地使用所需的模块 (Modules),减小应用程序的体积和依赖。理解 Qt 的模块化架构 (Modular Architecture),以及常用模块 (Modules) 的功能,有助于更好地使用 Qt 框架进行开发。

    Qt 框架包含众多模块 (Modules),以下介绍一些最常用的核心模块 (Modules)

    QtCore 模块 (Core Module): 💛 核心模块 (Core Module),是所有其他 Qt 模块的基础。QtCore 模块提供了 Qt 框架的核心非 GUI 功能,包括:

    ▮▮▮▮⚝ 核心对象模型 (Core Object Model)QObject 类、信号与槽 (Signals and Slots) 机制、属性系统 (Property System)对象树 (Object Tree)元对象系统 (Meta-Object System) 等。
    ▮▮▮▮⚝ 事件系统 (Event System): 事件循环 (Event Loop)、事件处理 (Event Handling)、事件过滤器 (Event Filter) 等。
    ▮▮▮▮⚝ 输入/输出 (Input/Output): 文件系统 (File System) 操作、文件 (Files) 和目录 (Directories) 处理、数据流 (Data Streams)、文本编解码 (Text Codecs) 等。
    ▮▮▮▮⚝ 数据类型与容器 (Data Types and Containers): Qt 特有的数据类型 (例如 QString, QByteArray, QVariant, QDate, QTime)、容器类 (例如 QList, QVector, QMap, QSet) 等。
    ▮▮▮▮⚝ 线程与并发 (Threads and Concurrency): 线程 (Threads) 管理、线程同步 (Thread Synchronization) 原语 (例如 QMutex, QSemaphore, QWaitCondition)、Qt 并发框架 (Qt Concurrent Framework) 等。
    ▮▮▮▮⚝ 定时器 (Timers): 单次定时器 (Single-Shot Timers)、周期性定时器 (Periodic Timers)。
    ▮▮▮▮⚝ 设置 (Settings): 应用程序设置 (Application Settings) 的读写操作。
    ▮▮▮▮⚝ JSON 和 XML 支持 (JSON and XML Support): JSON 数据的解析和生成、XML 数据的解析和生成。
    ▮▮▮▮⚝ 插件 (Plugins): 插件框架 (Plugin Framework) 支持。
    ▮▮▮▮⚝ 国际化与本地化 (Internationalization and Localization): 多语言支持、文本翻译、区域设置 (Locale) 处理等。
    ▮▮▮▮⚝ 命令行解析 (Command Line Parsing): 命令行参数解析工具。

    ▮▮▮▮⚝ 依赖关系: QtCore 模块不依赖于任何其他 Qt GUI 模块,是完全独立的。

    ▮▮▮▮⚝ 适用场景: 所有 Qt 应用程序都必须依赖 QtCore 模块,即使是纯 命令行 (Command-line) 程序或 服务器端 (Server-side) 程序也需要使用 QtCore 模块提供的核心功能。

    QtGui 模块 (GUI Module): 🎨 图形界面基础模块 (Graphical User Interface Base Module)。QtGui 模块提供了构建图形用户界面 (GUI) 的基础类和功能,包括:

    ▮▮▮▮⚝ 窗口系统集成 (Window System Integration): 与底层窗口系统 (例如 Windows, macOS, Linux X11/Wayland) 的接口,处理窗口 (Windows)、事件 (Events)、输入 (Input) 等。
    ▮▮▮▮⚝ 图形绘制 (Graphics and Painting): 2D 图形绘制 (2D Graphics Painting) 的基本类 (例如 QPainter, QPaintDevice, QPen, QBrush, QFont)。
    ▮▮▮▮⚝ 图像处理 (Image Handling): 图像 (Images) 的加载、保存、处理和显示 (例如 QImage, QPixmap, QPicture)。
    ▮▮▮▮⚝ 字体与文本 (Fonts and Text): 字体 (Fonts) 管理、文本 (Text) 渲染、文本布局 (Text Layout) 等。
    ▮▮▮▮⚝ 打印支持 (Printing Support): 打印 (Printing) 和打印预览 (Print Preview) 功能。
    ▮▮▮▮⚝ 拖放 (Drag and Drop): 拖放 (Drag and Drop) 操作的支持。
    ▮▮▮▮⚝ 剪贴板 (Clipboard): 剪贴板 (Clipboard) 操作的支持。
    ▮▮▮▮⚝ 光标与鼠标 (Cursors and Mouse): 光标 (Cursors) 管理、鼠标事件 (Mouse Events) 处理。
    ▮▮▮▮⚝ 键盘输入 (Keyboard Input): 键盘事件 (Keyboard Events) 处理、输入法 (Input Method) 支持。

    ▮▮▮▮⚝ 依赖关系: QtGui 模块依赖于 QtCore 模块。

    ▮▮▮▮⚝ 适用场景: 所有需要图形用户界面 (GUI) 的 Qt 应用程序都需要依赖 QtGui 模块。QtGui 模块提供了 GUI 应用程序的基础设施,但通常不直接提供具体的 GUI 控件 (Widgets)。

    QtWidgets 模块 (Widgets Module): 🎛️ 经典控件模块 (Classic Widgets Module)。QtWidgets 模块建立在 QtGui 模块之上,提供了丰富的 GUI 控件 (Widgets) 集合,用于构建传统的桌面风格 GUI 应用程序。QtWidgets 模块提供的控件都是基于 QWidget 类,使用 2D 绘图系统 (2D Painting System) 渲染。

    ▮▮▮▮⚝ 常用控件 (Common Widgets): 按钮 (Buttons) (例如 QPushButton, QRadioButton, QCheckBox)、标签 (Labels) (例如 QLabel)、文本框 (Text Editors) (例如 QLineEdit, QTextEdit)、容器 (Containers) (例如 QFrame, QGroupBox, QScrollArea)、布局管理器 (Layout Managers) (例如 QHBoxLayout, QVBoxLayout, QGridLayout)、对话框 (Dialogs) (例如 QMessageBox, QFileDialog, QColorDialog)、菜单栏 (Menu Bars) (例如 QMenuBar, QMenu, QAction)、工具栏 (Tool Bars) (例如 QToolBar)、状态栏 (Status Bars) (例如 QStatusBar)、进度条 (Progress Bars) (例如 QProgressBar)、滑块 (Sliders) (例如 QSlider)、微调框 (Spin Boxes) (例如 QSpinBox)、组合框 (Combo Boxes) (例如 QComboBox)、列表视图 (List Views) (例如 QListView, QListWidget)、表格视图 (Table Views) (例如 QTableView, QTableWidget)、树形视图 (Tree Views) (例如 QTreeView, QTreeWidget) 等。
    ▮▮▮▮⚝ 布局管理 (Layout Management): 布局管理器 (Layout Managers) 的实现,用于自动排列和调整控件 (Widgets) 的位置和大小。
    ▮▮▮▮⚝ 样式表 (Style Sheets)Qt 样式表 (Qt Style Sheets) (类似 CSS) 的支持,用于自定义控件 (Widgets) 的外观样式。
    ▮▮▮▮⚝ 输入部件 (Input Widgets): 各种用于用户输入的控件 (Widgets)。
    ▮▮▮▮⚝ 显示部件 (Display Widgets): 各种用于信息显示的控件 (Widgets)。
    ▮▮▮▮⚝ 容器部件 (Container Widgets): 用于组织和管理其他控件 (Widgets) 的容器控件 (Widgets)。
    ▮▮▮▮⚝ 窗口部件 (Window Widgets): 窗口 (Windows) 和对话框 (Dialogs) 的基类和实现。

    ▮▮▮▮⚝ 依赖关系: QtWidgets 模块依赖于 QtCore 和 QtGui 模块。

    ▮▮▮▮⚝ 适用场景: 大多数传统的桌面 GUI 应用程序都使用 QtWidgets 模块来构建用户界面。QtWidgets 模块提供了丰富的控件 (Widgets) 和布局管理器 (Layout Managers),方便快速开发功能完善的桌面应用程序。

    QtNetwork 模块 (Network Module): 🌐 网络编程模块 (Network Programming Module)。QtNetwork 模块提供了网络编程 (Network Programming) 相关的功能,包括:

    ▮▮▮▮⚝ TCP 和 UDP 支持 (TCP and UDP Support): TCP 套接字 (TCP Sockets) (例如 QTcpSocket, QTcpServer)、UDP 套接字 (UDP Sockets) (例如 QUdpSocket)。
    ▮▮▮▮⚝ HTTP 和 FTP 支持 (HTTP and FTP Support): HTTP 客户端 (HTTP Client) (例如 QNetworkAccessManager, QNetworkRequest, QNetworkReply)、FTP 客户端 (FTP Client)。
    ▮▮▮▮⚝ SSL/TLS 支持 (SSL/TLS Support): 安全套接层 (Secure Sockets Layer, SSL) 和传输层安全 (Transport Layer Security, TLS) 协议的支持,用于实现安全的网络通信。
    ▮▮▮▮⚝ 网络会话管理 (Network Session Management): 网络会话 (Network Session) 管理功能。
    ▮▮▮▮⚝ 网络地址和名称解析 (Network Address and Name Resolution): 网络地址 (Network Address) 和主机名 (Hostname) 的解析。
    ▮▮▮▮⚝ 代理 (Proxy): 网络代理 (Network Proxy) 支持。
    ▮▮▮▮⚝ Bearer Management: Bearer 令牌管理。

    ▮▮▮▮⚝ 依赖关系: QtNetwork 模块依赖于 QtCore 模块。

    ▮▮▮▮⚝ 适用场景: 需要网络通信功能的 Qt 应用程序,例如网络客户端 (例如 HTTP 客户端、FTP 客户端、自定义协议客户端)、网络服务器 (例如 TCP 服务器、HTTP 服务器) 等。

    QtSql 模块 (SQL Module): 🗄️ 数据库模块 (Database Module)。QtSql 模块提供了数据库访问 (Database Access) 相关的功能,允许 Qt 应用程序连接和操作各种 SQL 数据库。

    ▮▮▮▮⚝ 数据库驱动 (Database Drivers): 支持多种 SQL 数据库,例如 SQLite, MySQL, PostgreSQL, Oracle, ODBC, TDS (Microsoft SQL Server, Sybase) 等。通过 数据库驱动 (Database Drivers) 连接到不同的数据库系统。
    ▮▮▮▮⚝ SQL 查询 (SQL Queries): 执行 SQL 查询语句 (例如 QSqlQuery)、处理查询结果。
    ▮▮▮▮⚝ 数据库模型 (Database Models): 用于将数据库表数据绑定到 GUI 视图 (Views) 的数据模型 (Data Models) (例如 QSqlTableModel, QSqlRelationalTableModel)。
    ▮▮▮▮⚝ 事务处理 (Transaction Handling): 数据库事务 (Database Transaction) 的开始、提交、回滚操作。
    ▮▮▮▮⚝ 数据库连接管理 (Database Connection Management): 数据库连接 (Database Connection) 的创建、管理和复用 (例如 QSqlDatabase)。

    ▮▮▮▮⚝ 依赖关系: QtSql 模块依赖于 QtCore 模块。

    ▮▮▮▮⚝ 适用场景: 需要访问和操作数据库的 Qt 应用程序,例如数据管理系统、数据库客户端、需要持久化存储数据的应用程序等。

    除了以上介绍的常用核心模块 (Modules),Qt 框架还包含许多其他模块 (Modules),例如:

    QtMultimedia 模块 (Multimedia Module): 多媒体功能,包括音频 (Audio) 和视频 (Video) 的播放、录制、处理等。
    QtOpenGL 模块 (OpenGL Module) (已废弃,Qt 6 中被 Qt Rendering Hardware Interface (RHI) 取代): OpenGL 集成,用于 3D 图形渲染。
    QtPrintSupport 模块 (Print Support Module): 高级打印 (Advanced Printing) 功能,基于 QtGui 模块的打印支持进行扩展。
    QtQml 模块 (QML Module): QML 引擎和相关功能,用于使用 QML 语言进行声明式 UI 开发。
    QtQuick 模块 (Qt Quick Module): Qt Quick 框架的核心模块,基于 QML 语言和场景图 (Scene Graph) 技术,用于构建现代、流畅的 UI。
    QtSvg 模块 (SVG Module): SVG (Scalable Vector Graphics) 矢量图形的支持。
    QtWebEngine 模块 (Web Engine Module): 基于 Chromium 的 Web 浏览器引擎集成,用于在 Qt 应用程序中嵌入 Web 内容。
    QtWebSockets 模块 (WebSockets Module): WebSocket 协议的支持,用于实现实时的双向网络通信。
    QtXml 模块 (XML Module): XML 数据的解析和生成,基于 QtCore 模块的 XML 支持进行扩展。

    Qt 的模块化设计 (Modular Design) 使得框架的功能组织清晰、结构良好,开发者可以根据项目需求选择性地使用所需的模块 (Modules),提高了开发效率和应用程序的灵活性。

    2.4.2 模块的依赖关系与选择

    Qt 框架的模块 (Modules) 之间存在一定的依赖关系 (Dependencies)。理解模块 (Modules) 之间的依赖关系 (Dependencies),以及如何根据项目需求选择合适的模块 (Modules),是进行 Qt 开发的重要方面。

    模块依赖关系 (Module Dependencies)

    ▮▮▮▮⚝ 层次结构 (Hierarchical Structure): Qt 的模块 (Modules) 之间存在层次结构,一些模块 (Modules) 依赖于另一些模块 (Modules)。例如,QtGui 模块依赖于 QtCore 模块,QtWidgets 模块依赖于 QtCore 和 QtGui 模块。
    ▮▮▮▮⚝ QtCore 作为基础 (QtCore as Foundation): QtCore 模块是所有其他 Qt 模块的基础,几乎所有 Qt 模块都直接或间接地依赖于 QtCore 模块。QtCore 模块提供了 Qt 框架的核心基础设施,例如 对象模型 (Object Model)事件系统 (Event System)输入/输出 (Input/Output)线程 (Threads) 等。
    ▮▮▮▮⚝ GUI 模块依赖于核心模块 (GUI Modules Depend on Core Module): QtGui 模块和 QtWidgets 模块等 GUI 相关模块 (Modules) 都依赖于 QtCore 模块。QtGui 模块提供了 GUI 的基础类,QtWidgets 模块则建立在 QtGui 模块之上。
    ▮▮▮▮⚝ 功能模块依赖于核心模块和 GUI 模块 (Functional Modules Depend on Core and GUI Modules) (部分): 一些功能性模块 (Modules) (例如 QtNetwork, QtSql, QtMultimedia) 可能依赖于 QtCore 模块,也可能部分依赖于 QtGui 模块 (例如需要 GUI 功能的模块)。
    ▮▮▮▮⚝ 模块依赖关系文档 (Module Dependency Documentation): Qt 官方文档中明确指出了每个模块 (Module)依赖关系 (Dependencies)。在开发过程中,可以查阅 Qt 文档,了解模块 (Modules) 之间的依赖关系 (Dependencies)

    ▮▮▮▮⚝ 常见模块依赖关系图 (Simplified Dependency Graph) (仅供参考,具体依赖关系请查阅官方文档):

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QtCore (基础模块)
    2 ├── QtGui (图形界面基础模块)
    3 │ └── QtWidgets (经典控件模块)
    4 ├── QtNetwork (网络编程模块)
    5 ├── QtSql (数据库模块)
    6 ├── QtMultimedia (多媒体模块)
    7 ├── QtPrintSupport (打印支持模块)
    8 ├── QtQml (QML 模块)
    9 │ └── QtQuick (Qt Quick 模块)
    10 ├── QtSvg (SVG 模块)
    11 ├── QtWebEngine (Web Engine 模块)
    12 └── QtWebSockets (WebSockets 模块)

    模块选择 (Module Selection)

    ▮▮▮▮⚝ 根据项目需求选择模块 (Select Modules Based on Project Requirements): 在 Qt 项目开发中,应该根据项目的具体需求选择所需的 Qt 模块 (Modules)。不需要的功能模块可以不引入,减小应用程序的体积和编译时间。
    ▮▮▮▮⚝ Qt Creator 项目配置 (Qt Creator Project Configuration): 在 Qt Creator 中创建 Qt 项目时,可以选择需要包含的 Qt 模块 (Modules)。Qt Creator 会自动在项目配置文件 (例如 .pro 文件或 CMakeLists.txt 文件) 中添加所需的模块依赖。
    ▮▮▮▮⚝ .pro 文件配置 (.pro File Configuration) (对于 qmake 项目): 如果使用 qmake 构建系统,可以在 .pro 文件中使用 QT += module1 module2 module3 ... 的形式指定项目依赖的 Qt 模块 (Modules)。例如,要使用 QtCore, QtGui, QtWidgets 模块,可以在 .pro 文件中添加 QT += core gui widgets
    ▮▮▮▮⚝ CMakeLists.txt 文件配置 (CMakeLists.txt File Configuration) (对于 CMake 项目): 如果使用 CMake 构建系统,可以在 CMakeLists.txt 文件中使用 find_package(Qt6 COMPONENTS Module1 Module2 Module3 ... REQUIRED)find_package(Qt5 COMPONENTS Module1 Module2 Module3 ... REQUIRED) 的形式指定项目依赖的 Qt 模块 (Modules)。例如,要使用 QtCore, QtGui, QtWidgets 模块,可以在 CMakeLists.txt 文件中添加 find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED)find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED)
    ▮▮▮▮⚝ 显式链接模块库 (Explicitly Linking Module Libraries): 在编译和链接 Qt 应用程序时,需要将项目依赖的 Qt 模块 (Modules) 的库文件链接到最终的可执行程序中。构建系统 (例如 qmake, CMake) 会自动处理模块库的链接。
    ▮▮▮▮⚝ 运行时依赖 (Run-time Dependencies): Qt 应用程序在运行时 (Run-time) 需要依赖于所使用 Qt 模块 (Modules) 的动态链接库 (Dynamic Link Libraries, DLLs) 或共享库 (Shared Libraries)。在应用程序部署和发布时,需要将这些运行时依赖的库文件一同打包发布。

    模块选择示例:

    ▮▮▮▮⚝ 纯命令行应用程序 (Pure Command-line Application): 如果开发一个纯 命令行 (Command-line) 应用程序,只需要 QtCore 模块就足够了。例如,在 .pro 文件中配置 QT += core,或在 CMakeLists.txt 文件中配置 find_package(Qt6 COMPONENTS Core REQUIRED)find_package(Qt5 COMPONENTS Core REQUIRED)
    ▮▮▮▮⚝ 桌面 GUI 应用程序 (Desktop GUI Application): 如果开发一个传统的桌面 GUI 应用程序,通常需要 QtCore, QtGui, QtWidgets 模块。例如,在 .pro 文件中配置 QT += core gui widgets,或在 CMakeLists.txt 文件中配置 find_package(Qt6 COMPONENTS Core Gui Widgets REQUIRED)find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED)
    ▮▮▮▮⚝ 网络应用程序 (Network Application): 如果开发一个网络应用程序,需要 QtNetwork 模块。例如,在 .pro 文件中配置 QT += core network,或在 CMakeLists.txt 文件中配置 find_package(Qt6 COMPONENTS Core Network REQUIRED)find_package(Qt5 COMPONENTS Core Network REQUIRED)。如果同时需要 GUI 功能,则还需要添加 QtGui 和 QtWidgets 模块。
    ▮▮▮▮⚝ 数据库应用程序 (Database Application): 如果开发一个数据库应用程序,需要 QtSql 模块。例如,在 .pro 文件中配置 QT += core sql,或在 CMakeLists.txt 文件中配置 find_package(Qt6 COMPONENTS Core Sql REQUIRED)find_package(Qt5 COMPONENTS Core Sql REQUIRED)。如果同时需要 GUI 功能,则还需要添加 QtGui 和 QtWidgets 模块。
    ▮▮▮▮⚝ 现代 UI 应用程序 (Modern UI Application): 如果开发一个现代 UI 应用程序,可以使用 Qt Quick 框架,需要 QtCore, QtQml, QtQuick 模块。例如,在 .pro 文件中配置 QT += core qml quick,或在 CMakeLists.txt 文件中配置 find_package(Qt6 COMPONENTS Core Qml Quick REQUIRED)find_package(Qt5 COMPONENTS Core Qml Quick REQUIRED)

    总结:

    Qt 的模块化设计 (Modular Design) 使得框架结构清晰、功能组织良好。理解 Qt 模块 (Modules) 之间的依赖关系 (Dependencies),并根据项目需求选择合适的模块 (Modules),是进行高效 Qt 开发的关键。在项目配置中正确指定所需的 Qt 模块 (Modules),可以减小应用程序的体积、提高编译效率,并确保应用程序能够正常运行。

    3. 窗口与控件:构建用户界面

    3.1 窗口 (Window) 基础

    窗口 (Window) 是 GUI 应用程序的基本组成单元,它为用户提供了一个可视化的交互区域。在 Qt 中,窗口不仅仅是一个简单的容器,更是控件 (Widget) 的载体,以及用户与程序交互的桥梁。本节将介绍窗口的基本概念,以及如何在 Qt 框架下创建和管理窗口。

    3.1.1 顶层窗口 (Top-Level Windows) 与子窗口 (Child Windows)

    在 GUI 编程中,窗口可以分为两种主要类型:顶层窗口 (Top-Level Windows) 和子窗口 (Child Windows)。理解它们的区别和应用场景至关重要。

    顶层窗口 (Top-Level Windows)
    顶层窗口是独立于其他窗口存在的窗口,通常拥有操作系统的窗口边框和标题栏。
    ▮▮▮▮ⓐ 特点:
    ▮▮▮▮▮▮▮▮❷ 独立性:顶层窗口是应用程序的主窗口或独立的对话框,不依赖于其他窗口而存在。
    ▮▮▮▮▮▮▮▮❸ 系统管理:操作系统负责管理顶层窗口的生命周期、位置和外观,例如窗口的最小化、最大化和关闭等操作。
    ▮▮▮▮▮▮▮▮❹ 唯一性:每个顶层窗口在任务栏或Dock栏通常有独立的图标,代表一个独立的窗口实体。
    ▮▮▮▮ⓔ 用途:
    ▮▮▮▮▮▮▮▮❻ 主窗口 (Main Window):应用程序的主要交互界面,例如文档编辑器的主编辑窗口、浏览器的窗口等,通常继承自 QMainWindow 类。
    ▮▮▮▮▮▮▮▮❼ 对话框 (Dialog):用于执行特定任务或与用户进行简短交互的临时窗口,例如“打开文件”对话框、“关于”对话框等,通常继承自 QDialog 类。

    子窗口 (Child Windows)
    子窗口依赖于父窗口 (Parent Window) 而存在,它们嵌套在父窗口内部,并受父窗口的管理和约束。
    ▮▮▮▮ⓐ 特点:
    ▮▮▮▮▮▮▮▮❷ 依赖性:子窗口必须依附于一个父窗口,当父窗口被关闭或销毁时,子窗口也会随之销毁。
    ▮▮▮▮▮▮▮▮❸ 相对位置:子窗口的位置通常相对于其父窗口,并且被限制在父窗口的客户区域内。
    ▮▮▮▮▮▮▮▮❹ 内部组件:子窗口通常作为父窗口的组成部分,用于组织和展示更复杂的界面内容,例如工具栏、状态栏、控件组等。
    ▮▮▮▮ⓔ 用途:
    ▮▮▮▮▮▮▮▮❻ 控件容器:在父窗口中组织和布局各种控件,例如在一个 QWidget 子窗口中放置多个按钮和文本框。
    ▮▮▮▮▮▮▮▮❼ 界面分割:将复杂的界面分割成多个逻辑区域,每个区域可以使用一个子窗口来管理,提高代码的可维护性和可读性。
    ▮▮▮▮▮▮▮▮❽ MDI (多文档界面, Multiple Document Interface) 子窗口:在 MDI 应用程序中,每个文档可以拥有一个子窗口,这些子窗口都嵌套在主窗口内,例如早期的 Microsoft Office 软件。

    理解顶层窗口和子窗口的概念,有助于我们合理地组织和构建 GUI 应用程序的界面结构。在 Qt 中,窗口之间的父子关系是通过对象树 (Object Tree) 来管理的,这对于内存管理和事件传递都至关重要。

    3.1.2 QWidget 类:所有控件的基类

    QWidget 类是 Qt 框架中所有用户界面对象的基础类,它不仅是窗口的基类,也是所有控件 (Widgets) 的基类。理解 QWidget 类的作用和特性,是深入学习 Qt GUI 编程的关键。

    QWidget 的核心功能
    ▮▮▮▮ⓑ 窗口特性:QWidget 类本身就具备窗口的特性,可以作为顶层窗口或子窗口使用。它管理着窗口的几何属性(位置、大小)、外观 (样式、背景色) 和事件处理能力。
    ▮▮▮▮ⓒ 控件基类:Qt 中所有可见的用户界面元素,例如按钮 (QPushButton)、标签 (QLabel)、文本框 (QLineEdit) 等,都直接或间接地继承自 QWidget 类。这意味着 QWidget 类提供了所有控件都共有的基本功能和接口。
    ▮▮▮▮ⓓ 绘图能力:QWidget 类提供了绘图事件 (paintEvent),允许开发者在控件或窗口上进行自定义绘制,实现丰富的视觉效果。
    ▮▮▮▮ⓔ 事件处理:QWidget 类是事件驱动 (Event-driven) 编程模型的核心,它能够接收和处理各种用户交互事件,例如鼠标事件 (Mouse Events)、键盘事件 (Key Events) 等,并通过信号 (Signals) 和槽 (Slots) 机制进行事件响应。
    ▮▮▮▮ⓕ 布局管理:QWidget 类可以作为布局管理器 (Layout Managers) 的载体,使用布局管理器可以自动调整控件的大小和位置,适应窗口大小的变化,实现灵活的界面布局。

    QWidget 的重要属性和方法
    ▮▮▮▮ⓑ 几何属性:
    ▮▮▮▮▮▮▮▮❸ x(), y(): 获取控件在父窗口中的 x 和 y 坐标。
    ▮▮▮▮▮▮▮▮❹ width(), height(): 获取控件的宽度和高度。
    ▮▮▮▮▮▮▮▮❺ geometry(): 获取控件的几何矩形 (QRect),包括位置和大小。
    ▮▮▮▮▮▮▮▮❻ pos(): 获取控件的位置 (QPoint)。
    ▮▮▮▮▮▮▮▮❼ size(): 获取控件的大小 (QSize)。
    ▮▮▮▮ⓗ 外观属性:
    ▮▮▮▮▮▮▮▮❾ setWindowTitle(const QString &): 设置窗口标题。
    ▮▮▮▮▮▮▮▮❿ setStyleSheet(const QString &): 设置控件的样式表 (Style Sheet),用于美化界面。
    ▮▮▮▮▮▮▮▮❸ setBackgroundColor(const QColor &): 设置背景颜色。(注意:通常通过样式表或调色板 (Palette) 设置外观,而非直接使用此方法
    ▮▮▮▮ⓛ 状态属性:
    ▮▮▮▮▮▮▮▮❶ isEnabled(), setEnabled(bool): 获取和设置控件的启用状态。禁用状态下的控件通常会灰显,且无法响应用户交互。
    ▮▮▮▮▮▮▮▮❷ isVisible(), setVisible(bool): 获取和设置控件的可见状态。
    ▮▮▮▮ⓞ 父子关系:
    ▮▮▮▮▮▮▮▮❶ parentWidget(): 获取父控件。
    ▮▮▮▮▮▮▮▮❷ childAt(int x, int y): 获取指定位置的子控件。

    QWidget 与对象树
    在 Qt 中,QWidget 对象可以构成对象树 (Object Tree) 的一部分。当创建一个 QWidget 对象时,可以指定一个父 QWidget 对象,建立父子关系。
    ▮▮▮▮ⓐ 内存管理:Qt 的对象树机制实现了自动内存管理。当父对象被销毁时,其所有子对象也会被自动销毁,无需手动释放子对象的内存。这极大地简化了内存管理,降低了内存泄漏的风险。
    ▮▮▮▮ⓑ 事件传递:事件 (Event) 在对象树中可以传播。例如,如果一个鼠标事件没有被子控件处理,它会沿着对象树向上传递给父控件,直到被处理或到达顶层窗口。

    理解 QWidget 类的特性和功能,是掌握 Qt GUI 编程的基础。在后续章节中,我们将看到如何使用各种继承自 QWidget 的控件来构建丰富的用户界面。

    3.1.3 创建和显示窗口:QMainWindow, QDialog, QWidget

    Qt 提供了多个类来创建不同类型的窗口。最常用的窗口基类包括 QMainWindow, QDialog, 和 QWidget。选择合适的窗口类型是构建应用程序界面的第一步。

    QMainWindow 类:主窗口
    QMainWindow 类用于创建应用程序的主窗口 (Main Window)。主窗口通常是应用程序的核心,拥有菜单栏 (Menu Bar)、工具栏 (Tool Bar)、状态栏 (Status Bar) 和中心控件区域 (Central Widget Area)。
    ▮▮▮▮ⓐ 特点:
    ▮▮▮▮▮▮▮▮❷ 完整的主窗口结构:QMainWindow 提供了构建标准应用程序主窗口的完整框架,包括菜单栏、工具栏、停靠窗口 (Dock Widgets) 和中心控件区域。
    ▮▮▮▮▮▮▮▮❸ 菜单栏 (QMenuBar):用于放置应用程序的菜单项,例如“文件”、“编辑”、“视图”等菜单。
    ▮▮▮▮▮▮▮▮❹ 工具栏 (QToolBar):用于放置常用的操作按钮,例如“保存”、“复制”、“粘贴”等工具按钮。
    ▮▮▮▮▮▮▮▮❺ 状态栏 (QStatusBar):通常位于窗口底部,用于显示状态信息,例如操作提示、进度信息等。
    ▮▮▮▮▮▮▮▮❻ 中心控件 (Central Widget):主窗口的中心区域,用于放置主要的用户界面控件,例如文本编辑器、图像显示区域等。可以使用 setCentralWidget() 方法设置中心控件。
    ▮▮▮▮ⓖ 创建和显示 QMainWindow 窗口的步骤:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QMainWindow>
    3 #include <QLabel>
    4 #include <QMenuBar>
    5 #include <QMenu>
    6 #include <QAction>
    7
    8 int main(int argc, char *argv[]) {
    9 QApplication app(argc, argv);
    10
    11 QMainWindow mainWindow; // 创建主窗口对象
    12 mainWindow.setWindowTitle("QMainWindow 示例"); // 设置窗口标题
    13
    14 // 创建菜单栏
    15 QMenuBar *menuBar = mainWindow.menuBar();
    16 QMenu *fileMenu = menuBar->addMenu("文件(&F)"); // 创建“文件”菜单
    17 QAction *exitAction = fileMenu->addAction("退出(&X)"); // 创建“退出”菜单项
    18 QObject::connect(exitAction, &QAction::triggered, &app, &QApplication::quit); // 连接“退出”菜单项的信号到应用程序的退出槽
    19
    20 // 创建中心控件
    21 QLabel *label = new QLabel("Hello, QMainWindow!", &mainWindow); // 创建标签控件,并设置父窗口为主窗口
    22 mainWindow.setCentralWidget(label); // 将标签控件设置为中心控件
    23
    24 mainWindow.show(); // 显示主窗口
    25 return app.exec();
    26 }

    在这个例子中,我们创建了一个 QMainWindow 主窗口,设置了标题,添加了包含“退出”菜单项的“文件”菜单,并创建了一个标签控件作为中心控件显示在主窗口的中心区域。

    QDialog 类:对话框
    QDialog 类用于创建对话框 (Dialog)。对话框是用于执行特定任务或与用户进行临时交互的顶层窗口,通常是模态 (Modal) 或非模态 (Modeless) 的。
    ▮▮▮▮ⓐ 特点:
    ▮▮▮▮▮▮▮▮❷ 临时性交互:对话框通常用于执行一次性的、简短的用户交互,例如获取用户输入、显示警告信息、确认操作等。
    ▮▮▮▮▮▮▮▮❸ 模态与非模态:
    ▮▮▮▮▮▮▮▮ ❶ 模态对话框 (Modal Dialog):当模态对话框显示时,会阻塞父窗口 (或整个应用程序) 的用户输入。用户必须先关闭模态对话框才能继续操作父窗口。常用于重要的提示或需要用户立即响应的场景,例如错误提示对话框、保存确认对话框等。使用 exec() 方法显示模态对话框。
    ▮▮▮▮▮▮▮▮ ❷ 非模态对话框 (Modeless Dialog):非模态对话框显示时,不会阻塞父窗口的用户输入。用户可以同时与非模态对话框和父窗口进行交互。常用于工具窗口或需要长时间显示并与主窗口协同工作的场景,例如“查找和替换”对话框、属性设置对话框等。使用 show() 方法显示非模态对话框。
    ▮▮▮▮ⓑ 创建和显示 QDialog 对话框的步骤:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QDialog>
    3 #include <QLabel>
    4 #include <QVBoxLayout>
    5 #include <QPushButton>
    6
    7 int main(int argc, char *argv[]) {
    8 QApplication app(argc, argv);
    9
    10 QDialog dialog; // 创建对话框对象
    11 dialog.setWindowTitle("QDialog 示例"); // 设置对话框标题
    12
    13 QLabel *label = new QLabel("Hello, QDialog!", &dialog); // 创建标签控件,并设置父窗口为对话框
    14 QPushButton *closeButton = new QPushButton("关闭", &dialog); // 创建按钮控件,并设置父窗口为对话框
    15 QObject::connect(closeButton, &QPushButton::clicked, &dialog, &QDialog::close); // 连接按钮的点击信号到对话框的关闭槽
    16
    17 QVBoxLayout *layout = new QVBoxLayout(&dialog); // 创建垂直布局管理器,并设置对话框为父窗口
    18 layout->addWidget(label); // 添加标签控件到布局
    19 layout->addWidget(closeButton); // 添加按钮控件到布局
    20 dialog.setLayout(layout); // 设置对话框的布局
    21
    22 dialog.show(); // 显示非模态对话框 (使用 dialog.exec() 显示模态对话框)
    23 return app.exec();
    24 }

    在这个例子中,我们创建了一个 QDialog 对话框,设置了标题,创建了一个标签和一个按钮控件,使用 QVBoxLayout 垂直布局管理器将它们排列在对话框中,并连接了按钮的点击事件来关闭对话框。

    QWidget 类:通用窗口或控件容器
    QWidget 类本身也可以作为顶层窗口使用,或者作为其他窗口或控件的容器。当需要创建自定义的、结构简单的窗口,或者仅仅需要一个控件容器时,可以直接使用 QWidget 类。
    ▮▮▮▮ⓐ 特点:
    ▮▮▮▮▮▮▮▮❷ 通用性:QWidget 类是最通用的窗口和控件基类,可以用于创建各种自定义的用户界面元素。
    ▮▮▮▮▮▮▮▮❸ 灵活性:可以直接使用 QWidget 创建简单的顶层窗口,也可以将其作为容器控件,在其中添加其他控件并进行布局。
    ▮▮▮▮▮▮▮▮❹ 简洁性:相比于 QMainWindowQDialogQWidget 类更加轻量级,适用于创建结构简单的窗口或自定义控件。
    ▮▮▮▮ⓔ 创建和显示 QWidget 窗口的步骤:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QWidget>
    3 #include <QLabel>
    4 #include <QVBoxLayout>
    5
    6 int main(int argc, char *argv[]) {
    7 QApplication app(argc, argv);
    8
    9 QWidget window; // 创建 QWidget 窗口对象
    10 window.setWindowTitle("QWidget 示例"); // 设置窗口标题
    11
    12 QLabel *label = new QLabel("Hello, QWidget!", &window); // 创建标签控件,并设置父窗口为 QWidget 窗口
    13
    14 QVBoxLayout *layout = new QVBoxLayout(&window); // 创建垂直布局管理器,并设置 QWidget 窗口为父窗口
    15 layout->addWidget(label); // 添加标签控件到布局
    16 window.setLayout(layout); // 设置窗口的布局
    17
    18 window.show(); // 显示 QWidget 窗口
    19 return app.exec();
    20 }

    在这个例子中,我们创建了一个 QWidget 窗口,设置了标题,创建了一个标签控件,并使用 QVBoxLayout 垂直布局管理器将标签放置在窗口中。

    选择 QMainWindow, QDialog, 或 QWidget 取决于要创建的窗口类型和功能需求。QMainWindow 适用于构建具有完整主窗口结构的应用程序,QDialog 适用于创建对话框,而 QWidget 则更加通用和灵活,可以用于创建简单的窗口或作为控件容器。

    3.1.4 窗口的属性设置:标题、大小、位置、图标

    创建窗口后,通常需要设置窗口的各种属性,例如标题、大小、位置和图标等,以定制窗口的外观和行为。

    设置窗口标题
    使用 setWindowTitle(const QString &) 方法可以设置窗口的标题,标题会显示在窗口的标题栏上。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 mainWindow.setWindowTitle("我的应用程序标题");
    2 dialog.setWindowTitle("提示对话框");
    3 window.setWindowTitle("简单窗口");

    设置窗口大小
    可以使用以下方法设置窗口的大小:
    ▮▮▮▮ⓐ resize(int width, int height): 设置窗口的宽度和高度,单位为像素 (pixels)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 mainWindow.resize(800, 600); // 设置主窗口大小为 800x600
    2 dialog.resize(400, 300); // 设置对话框大小为 400x300

    ▮▮▮▮ⓑ setFixedSize(int width, int height): 设置窗口的固定大小,用户无法通过拖拽边框来改变窗口大小。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 dialog.setFixedSize(400, 300); // 设置对话框大小固定为 400x300

    ▮▮▮▮ⓒ setMinimumSize(int width, int height)setMaximumSize(int width, int height): 设置窗口的最小和最大尺寸限制。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 mainWindow.setMinimumSize(600, 400); // 设置主窗口最小尺寸为 600x400
    2 mainWindow.setMaximumSize(1024, 768); // 设置主窗口最大尺寸为 1024x768

    设置窗口位置
    可以使用以下方法设置窗口在屏幕上的位置:
    ▮▮▮▮ⓐ move(int x, int y): 将窗口移动到屏幕坐标 (x, y) 处。屏幕坐标原点通常位于屏幕左上角。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 mainWindow.move(100, 100); // 将主窗口移动到屏幕坐标 (100, 100)

    ▮▮▮▮ⓑ setGeometry(int x, int y, int width, int height): 同时设置窗口的位置和大小。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 mainWindow.setGeometry(100, 100, 800, 600); // 设置主窗口位置为 (100, 100),大小为 800x600

    ▮▮▮▮ⓒ center() (需要手动实现或使用辅助函数): 将窗口居中显示在屏幕上。Qt 本身没有直接的 center() 方法,但可以通过获取屏幕尺寸和窗口尺寸来计算居中位置。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 窗口居中显示的示例代码片段 (需要包含 <QScreen>)
    2 #include <QScreen>
    3 #include <QGuiApplication>
    4
    5 void centerWindow(QWidget *window) {
    6 QScreen *screen = QGuiApplication::primaryScreen();
    7 QRect screenGeometry = screen->geometry();
    8 int x = (screenGeometry.width() - window->width()) / 2;
    9 int y = (screenGeometry.height() - window->height()) / 2;
    10 window->move(x, y);
    11 }
    12
    13 // ... 在 main 函数中 ...
    14 centerWindow(&mainWindow); // 将主窗口居中显示

    设置窗口图标
    使用 setWindowIcon(const QIcon &) 方法可以设置窗口的图标,图标会显示在窗口的标题栏和任务栏 (或 Dock 栏) 上。需要使用 QIcon 类来加载图标文件。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QIcon>
    2
    3 // ... 在 main 函数中 ...
    4 QIcon icon(":/images/app_icon.png"); // 加载图标文件,":/images/app_icon.png" 是资源路径,需要将图标文件添加到 Qt 资源文件中
    5 mainWindow.setWindowIcon(icon); // 设置主窗口图标

    注意:需要将图标文件添加到 Qt 项目的资源文件 (例如 .qrc 文件) 中,并使用资源路径来加载图标。

    通过设置窗口的标题、大小、位置和图标等属性,可以定制窗口的外观,使其更符合应用程序的需求和用户期望。这些属性可以在创建窗口后随时修改,以动态地改变窗口的外观和行为。

    3.2 常用控件 (Widgets) 详解

    控件 (Widgets) 是 GUI 应用程序用户界面的基本构建块。Qt 提供了丰富的控件库,涵盖了各种常见的用户界面元素,例如按钮 (Button)、标签 (Label)、文本框 (LineEdit)、复选框 (CheckBox) 等。本节将详细介绍 Qt 中常用的一些控件及其使用方法。

    3.2.1 按钮 (QPushButton):事件响应与交互

    按钮 (QPushButton) 是 GUI 界面中最常用、最基础的交互控件之一。用户通过点击按钮来触发特定的操作或命令。QPushButton 提供了丰富的信号 (Signals) 和槽 (Slots) 机制,用于响应用户的点击事件和实现交互功能。

    QPushButton 的常用功能
    ▮▮▮▮ⓑ 文本显示:按钮上可以显示文本标签,用于描述按钮的功能。可以使用 setText(const QString &) 方法设置按钮的文本。
    ▮▮▮▮ⓒ 图标显示:按钮上可以显示图标,增强视觉效果和功能指示。可以使用 setIcon(const QIcon &) 方法设置按钮的图标。
    ▮▮▮▮ⓓ 快捷键 (Shortcut):可以为按钮设置快捷键,用户可以通过按下快捷键来触发按钮的点击事件。可以使用 setShortcut(const QKeySequence &) 方法设置快捷键。
    ▮▮▮▮ⓔ 自动重复 (Auto Repeat):可以设置按钮在长按时自动重复触发点击事件。可以使用 setAutoRepeat(bool) 方法启用或禁用自动重复功能。
    ▮▮▮▮ⓕ 默认按钮 (Default Button):在一个窗口或对话框中,可以设置一个按钮为默认按钮。当用户按下 Enter 键时,默认按钮会被自动点击。可以使用 setDefault(bool) 方法设置默认按钮。
    ▮▮▮▮ⓖ 扁平外观 (Flat Appearance):可以设置按钮为扁平外观,即按钮在默认状态下没有边框,只有在鼠标悬停或按下时才显示边框。可以使用 setFlat(bool) 方法设置扁平外观。

    QPushButton 的常用信号
    ▮▮▮▮ⓑ clicked(): 当按钮被点击 (鼠标按下并释放) 时发射此信号。这是最常用的按钮信号,用于响应用户的点击操作。
    ▮▮▮▮ⓒ pressed(): 当鼠标按键在按钮上按下时发射此信号。
    ▮▮▮▮ⓓ released(): 当鼠标按键在按钮上释放时发射此信号。
    ▮▮▮▮ⓔ toggled(bool checked): 当按钮的状态发生切换 (例如,如果按钮是可切换的,使用 setCheckable(true) 设置) 时发射此信号。checked 参数指示按钮的当前状态 (选中或未选中)。

    QPushButton 的使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QWidget>
    3 #include <QPushButton>
    4 #include <QVBoxLayout>
    5 #include <QMessageBox>
    6
    7 int main(int argc, char *argv[]) {
    8 QApplication app(argc, argv);
    9
    10 QWidget window;
    11 window.setWindowTitle("QPushButton 示例");
    12
    13 QPushButton *button1 = new QPushButton("点击我", &window); // 创建按钮 1
    14 QPushButton *button2 = new QPushButton("退出", &window); // 创建按钮 2
    15
    16 // 连接按钮 1 的 clicked 信号到 lambda 表达式槽函数
    17 QObject::connect(button1, &QPushButton::clicked, [](){
    18 QMessageBox::information(nullptr, "按钮点击", "按钮 1 被点击了!"); // 显示消息框
    19 });
    20
    21 // 连接按钮 2 的 clicked 信号到 QApplication 的 quit 槽
    22 QObject::connect(button2, &QPushButton::clicked, &app, &QApplication::quit);
    23
    24 QVBoxLayout *layout = new QVBoxLayout(&window);
    25 layout->addWidget(button1);
    26 layout->addWidget(button2);
    27 window.setLayout(layout);
    28
    29 window.show();
    30 return app.exec();
    31 }

    在这个例子中,我们创建了两个 QPushButton 按钮,并分别连接了它们的 clicked() 信号到不同的槽函数。当点击 “点击我” 按钮时,会弹出一个消息框;当点击 “退出” 按钮时,应用程序会退出。

    QPushButton 的样式设置
    QPushButton 的外观可以通过样式表 (Style Sheets) 或调色板 (Palette) 进行定制。例如,可以使用样式表来改变按钮的背景色、文本颜色、边框样式等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 button1->setStyleSheet("QPushButton {"
    2 " background-color: lightblue;"
    3 " border-style: outset;"
    4 " border-width: 2px;"
    5 " border-radius: 10px;"
    6 " border-color: beige;"
    7 " font: bold 14px;"
    8 " min-width: 10em;"
    9 " padding: 6px;"
    10 "}"
    11 "QPushButton:hover {"
    12 " background-color: qlineargradient(x1: 0, y1: 0, x2: 0, y2: 1,"
    13 " stop: 0 #f6f7fa, stop: 1 #dadbde);"
    14 "}"
    15 "QPushButton:pressed {"
    16 " border-style: inset;"
    17 "}");

    这段样式表代码为按钮设置了浅蓝色背景、外凸边框、圆角、粗体字体等样式,并定义了鼠标悬停和按下时的不同外观。

    QPushButton 是构建用户交互界面的重要控件,通过灵活地使用其信号和样式设置,可以创建各种功能丰富且美观的按钮。

    3.2.2 标签 (QLabel):显示文本与图像

    标签 (QLabel) 控件用于在 GUI 界面上显示文本或图像。QLabel 主要用于静态信息的展示,例如标题、说明文字、图片等,通常不直接响应用户交互事件(虽然也可以通过设置标志来使其响应)。

    QLabel 的常用功能
    ▮▮▮▮ⓑ 文本显示:QLabel 可以显示单行或多行文本。可以使用 setText(const QString &) 方法设置标签的文本内容。
    ▮▮▮▮ⓒ 富文本显示:QLabel 支持显示富文本 (Rich Text),可以使用 HTML 标签或 Qt 富文本格式来设置文本样式,例如字体、颜色、大小、粗体、斜体等。可以使用 setTextFormat(Qt::TextFormat) 方法设置文本格式,并使用 setText(const QString &) 方法设置包含富文本格式的文本内容。
    ▮▮▮▮ⓓ 图像显示:QLabel 可以显示图像。可以使用 setPixmap(const QPixmap &) 方法设置要显示的图像,QPixmap 类用于加载和管理图像数据。
    ▮▮▮▮ⓔ 动画 GIF 显示:QLabel 可以显示动画 GIF 图像。可以使用 setMovie(const QMovie &) 方法设置要显示的动画 GIF,QMovie 类用于加载和控制动画 GIF。
    ▮▮▮▮ⓕ 文本对齐方式:可以设置标签中文本的对齐方式,例如左对齐、居中对齐、右对齐等。可以使用 setAlignment(Qt::Alignment) 方法设置文本对齐方式。
    ▮▮▮▮ⓖ 文本换行模式:可以设置标签中文本的换行模式,例如自动换行、不换行、截断显示等。可以使用 setWordWrap(bool) 方法设置是否自动换行。
    ▮▮▮▮ⓗ 伙伴控件 (Buddy):可以为 QLabel 设置伙伴控件,通常用于与输入控件 (例如 QLineEdit, QSpinBox) 关联。当用户按下 Alt + 标签文本中带下划线的字符时,焦点会自动切换到伙伴控件。可以使用 setBuddy(QWidget *buddy) 方法设置伙伴控件。

    QLabel 的常用信号
    QLabel 本身通常不发射信号来响应用户交互,但可以通过设置标志 (Flags) 来使其发射信号,例如 linkActivated(const QString &) 信号在链接被点击时发射(需要使用富文本并包含链接)。

    QLabel 的使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QWidget>
    3 #include <QLabel>
    4 #include <QVBoxLayout>
    5 #include <QPixmap>
    6 #include <QMovie>
    7
    8 int main(int argc, char *argv[]) {
    9 QApplication app(argc, argv);
    10
    11 QWidget window;
    12 window.setWindowTitle("QLabel 示例");
    13
    14 QLabel *textLabel = new QLabel("这是一个普通的文本标签", &window); // 创建文本标签
    15 QLabel *richTextLabel = new QLabel("<p><font color='red' size='+2'><b>这是一个富文本</b></font><br/>"
    16 "<i>支持 HTML 标签</i></p>", &window); // 创建富文本标签
    17 richTextLabel->setTextFormat(Qt::RichText); // 设置文本格式为富文本
    18
    19 QLabel *imageLabel = new QLabel(&window); // 创建图像标签
    20 QPixmap pixmap(":/images/qt_logo.png"); // 加载图像,需要将 qt_logo.png 添加到资源文件
    21 imageLabel->setPixmap(pixmap.scaled(200, 200, Qt::KeepAspectRatio)); // 设置图像并缩放
    22
    23 QLabel *gifLabel = new QLabel(&window); // 创建 GIF 标签
    24 QMovie *movie = new QMovie(":/animations/loading.gif", &window); // 加载动画 GIF,需要将 loading.gif 添加到资源文件
    25 gifLabel->setMovie(movie); // 设置动画 GIF
    26 movie->start(); // 启动动画播放
    27
    28 QVBoxLayout *layout = new QVBoxLayout(&window);
    29 layout->addWidget(textLabel);
    30 layout->addWidget(richTextLabel);
    31 layout->addWidget(imageLabel);
    32 layout->addWidget(gifLabel);
    33 window.setLayout(layout);
    34
    35 window.show();
    36 return app.exec();
    37 }

    在这个例子中,我们创建了多个 QLabel 标签,分别用于显示普通文本、富文本、图像和动画 GIF。

    QLabel 的样式设置
    QLabel 的外观也可以通过样式表 (Style Sheets) 或调色板 (Palette) 进行定制,例如改变文本颜色、字体、背景色等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 textLabel->setStyleSheet("QLabel {"
    2 " color: blue;"
    3 " font: bold 16px;"
    4 "}");
    5 richTextLabel->setStyleSheet("QLabel {"
    6 " background-color: lightgray;"
    7 " padding: 5px;"
    8 " border: 1px solid gray;"
    9 "}");

    这些样式表代码分别为文本标签和富文本标签设置了不同的文本颜色、字体、背景色和边框样式。

    QLabel 是 GUI 界面中重要的信息展示控件,通过灵活地使用其文本、图像和样式设置功能,可以有效地向用户呈现各种静态信息。

    3.2.3 文本框 (QLineEdit):文本输入

    文本框 (QLineEdit) 控件用于接收单行文本输入。QLineEdit 提供了丰富的编辑功能和信号 (Signals),用于响应用户的文本输入和编辑操作。

    QLineEdit 的常用功能
    ▮▮▮▮ⓑ 文本输入与编辑:用户可以在 QLineEdit 中输入和编辑单行文本。
    ▮▮▮▮ⓒ 占位符文本 (Placeholder Text):可以设置占位符文本,当文本框为空时显示,提示用户输入内容。可以使用 setPlaceholderText(const QString &) 方法设置占位符文本。
    ▮▮▮▮ⓓ 文本显示模式 (Echo Mode):可以设置文本的显示模式,例如正常显示、密码模式 (显示为星号或圆点)、不显示等。可以使用 setEchoMode(QLineEdit::EchoMode) 方法设置显示模式。
    ▮▮▮▮▮▮▮▮❺ QLineEdit::Normal: 正常显示输入的文本。
    ▮▮▮▮▮▮▮▮❻ QLineEdit::Password: 以密码形式显示,通常显示为星号 * 或圆点
    ▮▮▮▮▮▮▮▮❼ QLineEdit::NoEcho: 不显示任何输入的文本,常用于需要完全隐藏用户输入的场景。
    ▮▮▮▮▮▮▮▮❽ QLineEdit::PasswordEchoOnEdit: 在编辑时显示正常文本,失去焦点后显示为密码形式。
    ▮▮▮▮ⓘ 文本长度限制:可以设置文本框允许输入的最大文本长度。可以使用 setMaxLength(int) 方法设置最大长度。
    ▮▮▮▮ⓙ 输入校验器 (Input Validator):可以使用输入校验器 (Validator) 来限制用户可以输入的字符类型和格式。Qt 提供了 QIntValidator (整数校验器)、QDoubleValidator (浮点数校验器) 和 QRegExpValidator (正则表达式校验器) 等内置校验器。可以使用 setValidator(const QValidator *validator) 方法设置校验器。
    ▮▮▮▮ⓚ 自动完成 (Auto-Completion):可以为 QLineEdit 设置自动完成器 (Completer),根据用户已输入的文本提供自动完成建议。可以使用 setCompleter(QCompleter *c) 方法设置自动完成器。
    ▮▮▮▮ⓛ 上下文菜单 (Context Menu):QLineEdit 默认提供上下文菜单,包含常用的编辑操作,例如剪切、复制、粘贴、撤销、重做等。可以通过 setContextMenuPolicy(Qt::ContextMenuPolicy) 方法自定义上下文菜单策略,甚至禁用默认菜单并自定义菜单。

    QLineEdit 的常用信号
    ▮▮▮▮ⓑ textChanged(const QString &text): 当文本框中的文本内容发生改变时,每次改变都会发射此信号,参数 text 是当前的文本内容。
    ▮▮▮▮ⓒ textEdited(const QString &text): 当文本框中的文本被用户编辑 (例如,用户输入或删除字符) 时发射此信号,参数 text 是编辑后的文本内容。与 textChanged() 信号类似,但 textEdited() 通常只在用户交互编辑时发射,而 textChanged() 在程序代码通过 setText() 方法改变文本时也会发射。
    ▮▮▮▮ⓓ returnPressed(): 当用户在文本框中按下 Enter 或 Return 键时发射此信号。常用于在用户完成输入后触发操作,例如提交表单、搜索等。
    ▮▮▮▮ⓔ editingFinished(): 当文本框的编辑完成时发射此信号。编辑完成的条件通常是文本框失去焦点 (例如,用户点击了其他控件或按下了 Tab 键)。
    ▮▮▮▮ⓕ cursorPositionChanged(int oldPos, int newPos): 当文本框中光标 (Cursor) 的位置发生改变时发射此信号,参数 oldPos 是旧的光标位置,newPos 是新的光标位置。
    ▮▮▮▮ⓖ selectionChanged(): 当文本框中的文本选区发生改变时发射此信号 (例如,用户使用鼠标或键盘选中了一段文本)。

    QLineEdit 的使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QWidget>
    3 #include <QLineEdit>
    4 #include <QLabel>
    5 #include <QVBoxLayout>
    6 #include <QIntValidator>
    7 #include <QCompleter>
    8 #include <QStringList>
    9
    10 int main(int argc, char *argv[]) {
    11 QApplication app(argc, argv);
    12
    13 QWidget window;
    14 window.setWindowTitle("QLineEdit 示例");
    15
    16 QLineEdit *lineEdit = new QLineEdit(&window); // 创建文本框
    17 lineEdit->setPlaceholderText("请输入文本"); // 设置占位符文本
    18 lineEdit->setEchoMode(QLineEdit::Normal); // 设置为正常显示模式
    19 lineEdit->setMaxLength(20); // 设置最大文本长度为 20
    20
    21 QIntValidator *validator = new QIntValidator(0, 100, &window); // 创建整数校验器,限制输入 0-100 的整数
    22 lineEdit->setValidator(validator); // 设置输入校验器
    23
    24 QStringList wordList = {"apple", "banana", "cherry", "date", "elderberry"}; // 创建自动完成的单词列表
    25 QCompleter *completer = new QCompleter(wordList, &window); // 创建自动完成器
    26 lineEdit->setCompleter(completer); // 设置自动完成器
    27
    28 QLabel *displayLabel = new QLabel("", &window); // 创建标签,用于显示文本框内容
    29
    30 // 连接 textChanged 信号到 lambda 表达式槽函数,实时显示文本框内容
    31 QObject::connect(lineEdit, &QLineEdit::textChanged, [&](const QString &text){
    32 displayLabel->setText("当前文本: " + text);
    33 });
    34
    35 // 连接 returnPressed 信号到 lambda 表达式槽函数,在按下 Enter 键时显示消息框
    36 QObject::connect(lineEdit, &QLineEdit::returnPressed, [](){
    37 QMessageBox::information(nullptr, "Enter 键", "你按下了 Enter 键!");
    38 });
    39
    40 QVBoxLayout *layout = new QVBoxLayout(&window);
    41 layout->addWidget(lineEdit);
    42 layout->addWidget(displayLabel);
    43 window.setLayout(layout);
    44
    45 window.show();
    46 return app.exec();
    47 }

    在这个例子中,我们创建了一个 QLineEdit 文本框,设置了占位符文本、正常显示模式、最大长度、整数校验器和自动完成器。同时,连接了 textChanged()returnPressed() 信号,分别用于实时显示文本框内容和在按下 Enter 键时弹出消息框。

    QLineEdit 的样式设置
    QLineEdit 的外观也可以通过样式表 (Style Sheets) 或调色板 (Palette) 进行定制,例如改变背景色、边框样式、文本颜色、字体等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 lineEdit->setStyleSheet("QLineEdit {"
    2 " border: 2px solid gray;"
    3 " border-radius: 5px;"
    4 " padding: 5px;"
    5 " background-color: white;"
    6 " selection-background-color: lightblue;" // 设置选中文本的背景色
    7 "}");

    这段样式表代码为文本框设置了灰色边框、圆角、白色背景和浅蓝色选中文本背景色等样式。

    QLineEdit 是 GUI 界面中重要的文本输入控件,通过灵活地使用其各种功能和信号,可以创建各种文本输入交互场景,例如用户登录、表单填写、搜索输入等。

    后续章节将继续详细介绍 QTextEdit, QCheckBox, QRadioButton, QComboBox, QListWidget, QSlider, QSpinBox, QProgressBar, QDial 等常用控件,以及控件属性和样式设置。

    4. 布局管理:灵活的界面设计

    本章将深入探讨 Qt 的布局管理系统 (Layout Management System),旨在帮助读者掌握创建能够灵活适应不同窗口大小和分辨率的用户界面的关键技术。通过本章的学习,读者将能够摆脱硬编码像素值的束缚,构建出在各种屏幕尺寸下都能保持良好用户体验的 GUI (图形用户界面)。

    4.1 布局管理器 (Layout Managers) 概述

    在 GUI 编程中,布局管理 是一个至关重要的概念。它指的是自动安排窗口中控件 (widgets) 的位置和大小,以便在用户调整窗口大小或更改显示设置时,界面元素能够合理地重新排列和缩放。Qt 框架提供了一套强大而灵活的 布局管理器,极大地简化了界面布局的设计工作。

    4.1.1 为什么需要布局管理器?

    在早期的 GUI 开发中,开发者通常需要手动计算并设置每个控件的精确位置和大小。这种 手动布局 方式存在诸多缺点:

    界面僵硬,缺乏灵活性:手动布局的界面在窗口大小改变时,控件的位置和大小不会自动调整,容易出现控件重叠、控件超出窗口边界等问题,导致界面布局混乱,用户体验差。

    维护困难:当界面需求发生变化,例如需要添加、删除或移动控件时,手动布局的代码需要大量修改,维护成本高昂且容易出错。

    跨平台兼容性差:不同操作系统和显示设备对控件的默认大小和间距可能存在差异,手动布局的界面难以保证在不同平台上的一致性。

    开发效率低下:手动计算和调整控件的位置和大小是一项繁琐且耗时的任务,严重影响开发效率。

    布局管理器 的出现正是为了解决这些问题。它允许开发者将控件添加到布局管理器中,然后由布局管理器自动处理控件的位置和大小调整。使用布局管理器具有以下显著优点:

    界面自适应:布局管理器能够根据窗口大小的变化自动调整控件的位置和大小,确保界面在不同尺寸的窗口中都能保持良好的布局效果。

    易于维护:使用布局管理器后,界面的修改变得更加简单。添加、删除或移动控件时,布局管理器会自动重新计算布局,开发者无需手动调整大量代码。

    跨平台兼容性:Qt 的布局管理器在不同平台上表现一致,有助于提高应用程序的跨平台兼容性。

    提高开发效率:布局管理器大大简化了界面布局的设计过程,开发者可以将更多精力集中在业务逻辑的实现上,从而提高开发效率。

    总之,布局管理器 是现代 GUI 编程中不可或缺的工具,它能够帮助开发者构建灵活、易于维护且跨平台兼容的用户界面。

    4.1.2 Qt 的布局管理器类型

    Qt 提供了多种布局管理器,每种布局管理器都有其特定的布局方式和适用场景。常用的 Qt 布局管理器主要包括以下几种:

    QHBoxLayout (水平布局管理器):将控件 水平 排列成一行。控件从左到右依次排列,可以设置控件之间的间距。

    QVBoxLayout (垂直布局管理器):将控件 垂直 排列成一列。控件从上到下依次排列,可以设置控件之间的间距。

    QGridLayout (网格布局管理器):将控件放置在一个 网格 中。可以精确控制控件在网格中的行和列位置,以及控件跨越的行数和列数。适用于需要创建复杂表格状布局的场景。

    QFormLayout (表单布局管理器):专门用于创建 表单 类型的布局,通常用于排列标签 (labels) 和输入控件 (input widgets) 对。标签通常左对齐,输入控件右对齐,形成清晰的表单结构。

    QStackedLayout (堆叠布局管理器):将多个控件 堆叠 在一起,同一时刻只显示一个控件。可以通过代码切换当前显示的控件,常用于实现 向导 界面或 页面切换 效果。

    除了以上常用的布局管理器,Qt 还提供了一些其他的布局管理器,例如:

    QBoxLayoutQHBoxLayoutQVBoxLayout 的基类,提供了通用的盒式布局功能。
    QGridLayout:更高级的网格布局管理器,提供了更灵活的网格布局控制。
    QSplitter:允许用户 拖动 控件之间的边界来调整控件大小的布局管理器。
    QScrollArea:当内容超出可用空间时,提供 滚动条 的布局管理器。

    在实际开发中,开发者可以根据具体的界面需求选择合适的布局管理器,也可以 嵌套组合 使用多种布局管理器,构建出复杂而灵活的用户界面。

    4.2 常用布局管理器详解与应用

    本节将详细介绍 QHBoxLayoutQVBoxLayoutQGridLayoutQFormLayoutQStackedLayout 这五种常用布局管理器的使用方法和应用场景,并通过代码示例进行演示。

    4.2.1 QHBoxLayoutQVBoxLayout:水平与垂直布局

    QHBoxLayoutQVBoxLayout 是最基础也是最常用的两种布局管理器,分别用于创建水平和垂直方向的线性布局。

    QHBoxLayout (水平布局)

    QHBoxLayout 将控件水平排列,控件从左到右依次添加。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtWidgets>
    2
    3 int main(int argc, char *argv[]) {
    4 QApplication app(argc, argv);
    5
    6 QWidget window;
    7 QHBoxLayout *layout = new QHBoxLayout; // ① 创建 QHBoxLayout 实例
    8
    9 QPushButton *button1 = new QPushButton("Button 1");
    10 QPushButton *button2 = new QPushButton("Button 2");
    11 QPushButton *button3 = new QPushButton("Button 3");
    12
    13 layout->addWidget(button1); // ② 添加控件到布局管理器
    14 layout->addWidget(button2);
    15 layout->addWidget(button3);
    16
    17 window.setLayout(layout); // ③ 将布局管理器设置给窗口
    18 window.setWindowTitle("QHBoxLayout Example");
    19 window.show();
    20
    21 return app.exec();
    22 }

    代码解释:

    QHBoxLayout *layout = new QHBoxLayout;:创建 QHBoxLayout 布局管理器的实例。

    layout->addWidget(button1); layout->addWidget(button2); layout->addWidget(button3);:使用 addWidget() 函数将三个按钮控件添加到 layout 布局管理器中。控件将按照添加的顺序从左到右水平排列。

    window.setLayout(layout);:使用 QWidgetsetLayout() 函数将 layout 布局管理器设置给 window 窗口。这样,window 窗口的布局就由 QHBoxLayout 管理了。

    QVBoxLayout (垂直布局)

    QVBoxLayout 将控件垂直排列,控件从上到下依次添加。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtWidgets>
    2
    3 int main(int argc, char *argv[]) {
    4 QApplication app(argc, argv);
    5
    6 QWidget window;
    7 QVBoxLayout *layout = new QVBoxLayout; // ① 创建 QVBoxLayout 实例
    8
    9 QPushButton *button1 = new QPushButton("Button 1");
    10 QPushButton *button2 = new QPushButton("Button 2");
    11 QPushButton *button3 = new QPushButton("Button 3");
    12
    13 layout->addWidget(button1); // ② 添加控件到布局管理器
    14 layout->addWidget(button2);
    15 layout->addWidget(button3);
    16
    17 window.setLayout(layout); // ③ 将布局管理器设置给窗口
    18 window.setWindowTitle("QVBoxLayout Example");
    19 window.show();
    20
    21 return app.exec();
    22 }

    代码结构与 QHBoxLayout 示例类似,只是将 QHBoxLayout 替换为 QVBoxLayout。运行结果会看到三个按钮控件从上到下垂直排列。

    常用 API (应用程序编程接口)

    addWidget(QWidget *widget, int stretch = 0, Qt::Alignment alignment = Qt::Alignment()):将控件 widget 添加到布局中。
    ▮▮▮▮⚝ stretch 参数:设置控件的 伸缩因子。伸缩因子决定了控件在布局管理器分配剩余空间时的权重。伸缩因子越大,控件占据的剩余空间比例越高。默认为 0,表示不伸缩。
    ▮▮▮▮⚝ alignment 参数:设置控件在布局单元格内的 对齐方式。例如 Qt::AlignLeft (左对齐), Qt::AlignCenter (居中对齐), Qt::AlignRight (右对齐), Qt::AlignTop (顶端对齐), Qt::AlignBottom (底端对齐)。

    addStretch(int stretch = 1):添加一个 伸缩空间 (stretch)。伸缩空间会占据布局管理器中的剩余空间,可以用于控制控件之间的间距和布局的整体伸缩行为。stretch 参数设置伸缩因子的权重,默认为 1。

    addSpacing(int size):添加一个固定大小的 空白间隔 (spacing)。size 参数指定间隔的大小,单位为像素。

    应用场景

    QHBoxLayout:适用于创建水平方向排列的工具栏、按钮组等。
    QVBoxLayout:适用于创建垂直方向排列的菜单栏、侧边栏、对话框布局等。
    QHBoxLayoutQVBoxLayout 经常 嵌套 使用,以构建更复杂的布局结构。例如,可以使用 QVBoxLayout 作为主布局,然后在 QVBoxLayout 中添加多个 QHBoxLayoutQVBoxLayout,实现更精细的布局控制。

    4.2.2 QGridLayout:网格布局的精细控制

    QGridLayout 布局管理器将控件放置在一个 二维网格 中。开发者可以指定控件所在的 行 (row)列 (column) 位置,以及控件 跨越的行数 (rowSpan)列数 (columnSpan)QGridLayout 提供了强大的网格布局控制能力,适用于创建复杂的表格状布局,例如计算器界面、复杂的配置界面等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtWidgets>
    2
    3 int main(int argc, char *argv[]) {
    4 QApplication app(argc, argv);
    5
    6 QWidget window;
    7 QGridLayout *layout = new QGridLayout; // ① 创建 QGridLayout 实例
    8
    9 // 创建按钮
    10 QPushButton *button0 = new QPushButton("0");
    11 QPushButton *button1 = new QPushButton("1");
    12 QPushButton *button2 = new QPushButton("2");
    13 QPushButton *button3 = new QPushButton("3");
    14 QPushButton *button4 = new QPushButton("4");
    15 QPushButton *button5 = new QPushButton("5");
    16 QPushButton *button6 = new QPushButton("6");
    17 QPushButton *button7 = new QPushButton("7");
    18 QPushButton *button8 = new QPushButton("8");
    19 QPushButton *button9 = new QPushButton("9");
    20 QPushButton *buttonPlus = new QPushButton("+");
    21 QPushButton *buttonMinus = new QPushButton("-");
    22 QPushButton *buttonMultiply = new QPushButton("*");
    23 QPushButton *buttonDivide = new QPushButton("/");
    24 QPushButton *buttonEqual = new QPushButton("=");
    25 QPushButton *buttonClear = new QPushButton("C");
    26
    27 // ② 添加控件到网格布局,并指定行、列位置
    28 layout->addWidget(button7, 0, 0); // button7 位于第 0 行,第 0 列
    29 layout->addWidget(button8, 0, 1);
    30 layout->addWidget(button9, 0, 2);
    31 layout->addWidget(buttonDivide, 0, 3);
    32
    33 layout->addWidget(button4, 1, 0); // button4 位于第 1 行,第 0 列
    34 layout->addWidget(button5, 1, 1);
    35 layout->addWidget(button6, 1, 2);
    36 layout->addWidget(buttonMultiply, 1, 3);
    37
    38 layout->addWidget(button1, 2, 0); // button1 位于第 2 行,第 0 列
    39 layout->addWidget(button2, 2, 1);
    40 layout->addWidget(button3, 2, 2);
    41 layout->addWidget(buttonMinus, 2, 3);
    42
    43 layout->addWidget(button0, 3, 0); // button0 位于第 3 行,第 0 列
    44 layout->addWidget(buttonClear, 3, 1);
    45 layout->addWidget(buttonEqual, 3, 2);
    46 layout->addWidget(buttonPlus, 3, 3);
    47
    48
    49 window.setLayout(layout); // ③ 将布局管理器设置给窗口
    50 window.setWindowTitle("QGridLayout Example");
    51 window.show();
    52
    53 return app.exec();
    54 }

    代码解释:

    QGridLayout *layout = new QGridLayout;:创建 QGridLayout 布局管理器的实例。

    layout->addWidget(button7, 0, 0); 等:使用 addWidget() 函数将按钮控件添加到 layout 网格布局中。addWidget() 函数的参数 rowcolumn 指定了控件在网格中的行和列位置,行号和列号都从 0 开始计数

    window.setLayout(layout);:将 layout 布局管理器设置给 window 窗口。

    常用 API

    addWidget(QWidget *widget, int row, int column, int rowSpan = 1, int columnSpan = 1, Qt::Alignment alignment = Qt::Alignment()):将控件 widget 添加到网格布局的 (row, column) 位置。
    ▮▮▮▮⚝ row 参数:控件所在的行号,从 0 开始计数。
    ▮▮▮▮⚝ column 参数:控件所在的列号,从 0 开始计数。
    ▮▮▮▮⚝ rowSpan 参数:控件 跨越的行数,默认为 1。
    ▮▮▮▮⚝ columnSpan 参数:控件 跨越的列数,默认为 1。
    ▮▮▮▮⚝ alignment 参数:设置控件在网格单元格内的对齐方式。

    setRowStretch(int row, int stretch):设置指定 伸缩因子。伸缩因子决定了行在网格布局分配剩余垂直空间时的权重。

    setColumnStretch(int column, int stretch):设置指定 伸缩因子。伸缩因子决定了列在网格布局分配剩余水平空间时的权重。

    setRowMinimumHeight(int row, int minSize):设置指定行的 最小高度

    setColumnMinimumWidth(int column, int minSize):设置指定列的 最小宽度

    应用场景

    ⚝ 创建 计算器 界面、键盘 界面等表格状布局。
    ⚝ 构建 复杂配置对话框,将配置项和输入控件整齐地排列在网格中。
    ⚝ 实现需要精确控制控件位置和大小的界面布局。

    4.2.3 QFormLayout:表单布局的快速创建

    QFormLayout 布局管理器专门用于创建 表单 类型的布局。它将控件排列成两列,通常 第一列 用于放置 标签 (labels)第二列 用于放置与标签对应的 输入控件 (input widgets),例如 QLineEdit (单行文本框), QComboBox (下拉框), QSpinBox (微调框) 等。QFormLayout 能够自动对齐标签和输入控件,快速创建清晰规范的表单界面。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtWidgets>
    2
    3 int main(int argc, char *argv[]) {
    4 QApplication app(argc, argv);
    5
    6 QWidget window;
    7 QFormLayout *layout = new QFormLayout; // ① 创建 QFormLayout 实例
    8
    9 // 创建标签和输入控件
    10 QLabel *nameLabel = new QLabel("姓名(&N):"); // 标签,使用 &N 设置快捷键 Alt+N
    11 QLineEdit *nameLineEdit = new QLineEdit;
    12 nameLabel->setBuddy(nameLineEdit); // 设置伙伴控件,使快捷键生效
    13
    14 QLabel *ageLabel = new QLabel("年龄(&A):"); // 标签,使用 &A 设置快捷键 Alt+A
    15 QSpinBox *ageSpinBox = new QSpinBox;
    16 ageLabel->setBuddy(ageSpinBox); // 设置伙伴控件,使快捷键生效
    17
    18 QLabel *emailLabel = new QLabel("邮箱(&E):"); // 标签,使用 &E 设置快捷键 Alt+E
    19 QLineEdit *emailLineEdit = new QLineEdit;
    20 emailLabel->setBuddy(emailLineEdit); // 设置伙伴控件,使快捷键生效
    21
    22
    23 // ② 添加表单项到布局管理器
    24 layout->addRow(nameLabel, nameLineEdit); // 添加一行表单项:标签为 nameLabel,控件为 nameLineEdit
    25 layout->addRow(ageLabel, ageSpinBox); // 添加一行表单项:标签为 ageLabel,控件为 ageSpinBox
    26 layout->addRow(emailLabel, emailLineEdit); // 添加一行表单项:标签为 emailLabel,控件为 emailLineEdit
    27
    28
    29 window.setLayout(layout); // ③ 将布局管理器设置给窗口
    30 window.setWindowTitle("QFormLayout Example");
    31 window.show();
    32
    33 return app.exec();
    34 }

    代码解释:

    QFormLayout *layout = new QFormLayout;:创建 QFormLayout 布局管理器的实例。

    layout->addRow(nameLabel, nameLineEdit); 等:使用 addRow() 函数添加表单项。addRow() 函数的第一个参数是标签控件,第二个参数是输入控件。QFormLayout 会自动将标签控件放置在左侧列,输入控件放置在右侧列,并对齐排列。

    window.setLayout(layout);:将 layout 布局管理器设置给 window 窗口。

    常用 API

    addRow(QWidget *label, QWidget *field):添加一行表单项,包含标签 label 和输入控件 field

    addRow(const QString &labelText, QWidget *field):添加一行表单项,标签文本为 labelText,输入控件为 field

    setLabelAlignment(Qt::Alignment alignment):设置 标签列对齐方式,默认为 Qt::AlignRight|Qt::AlignVCenter (右对齐,垂直居中)。

    setFieldAlignment(Qt::Alignment alignment):设置 输入控件列对齐方式,默认为 Qt::AlignLeft|Qt::AlignVCenter (左对齐,垂直居中)。

    setFormAlignment(Qt::Alignment alignment):设置 表单整体对齐方式,默认为 Qt::AlignLeft|Qt::AlignTop (左上角对齐)。

    setSpacing(int spacing):设置 行之间垂直间距

    setLabelSpacing(int spacing):设置 标签列输入控件列 之间的 水平间距

    应用场景

    ⚝ 创建 用户注册登录信息编辑 等表单界面。
    ⚝ 构建 软件设置参数配置 等配置对话框。
    ⚝ 适用于需要清晰组织标签和输入控件对的界面布局。

    4.2.4 QStackedLayout:堆叠布局与页面切换

    QStackedLayout 布局管理器将多个控件 堆叠 在一起,类似于 扑克牌堆。在同一时刻,QStackedLayout 只会显示 最顶层 的一个控件,其他控件都被隐藏。开发者可以通过代码 切换 当前显示的控件,从而实现 页面切换向导 界面效果。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtWidgets>
    2
    3 int main(int argc, char *argv[]) {
    4 QApplication app(argc, argv);
    5
    6 QWidget window;
    7 QStackedLayout *layout = new QStackedLayout; // ① 创建 QStackedLayout 实例
    8
    9 // 创建多个页面控件
    10 QWidget *page1 = new QWidget;
    11 QLabel *label1 = new QLabel("Page 1");
    12 QVBoxLayout *page1Layout = new QVBoxLayout;
    13 page1Layout->addWidget(label1);
    14 page1->setLayout(page1Layout);
    15
    16 QWidget *page2 = new QWidget;
    17 QLabel *label2 = new QLabel("Page 2");
    18 QVBoxLayout *page2Layout = new QVBoxLayout;
    19 page2Layout->addWidget(label2);
    20 page2->setLayout(page2Layout);
    21
    22 QWidget *page3 = new QWidget;
    23 QLabel *label3 = new QLabel("Page 3");
    24 QVBoxLayout *page3Layout = new QVBoxLayout;
    25 page3Layout->addWidget(label3);
    26 page3->setLayout(page3Layout);
    27
    28
    29 // ② 添加页面控件到堆叠布局管理器
    30 layout->addWidget(page1); // 添加页面 1
    31 layout->addWidget(page2); // 添加页面 2
    32 layout->addWidget(page3); // 添加页面 3
    33
    34 // 创建按钮用于切换页面
    35 QPushButton *buttonPage1 = new QPushButton("Page 1");
    36 QPushButton *buttonPage2 = new QPushButton("Page 2");
    37 QPushButton *buttonPage3 = new QPushButton("Page 3");
    38
    39 QHBoxLayout *buttonLayout = new QHBoxLayout;
    40 buttonLayout->addWidget(buttonPage1);
    41 buttonLayout->addWidget(buttonPage2);
    42 buttonLayout->addWidget(buttonPage3);
    43
    44 QVBoxLayout *mainLayout = new QVBoxLayout;
    45 mainLayout->addLayout(buttonLayout);
    46 mainLayout->addLayout(layout); // 将堆叠布局添加到主布局中
    47 window.setLayout(mainLayout); // 将主布局设置给窗口
    48
    49
    50 // ③ 连接按钮信号与槽,实现页面切换
    51 QObject::connect(buttonPage1, &QPushButton::clicked, [=]() {
    52 layout->setCurrentIndex(0); // 切换到索引为 0 的页面 (Page 1)
    53 });
    54 QObject::connect(buttonPage2, &QPushButton::clicked, [=]() {
    55 layout->setCurrentIndex(1); // 切换到索引为 1 的页面 (Page 2)
    56 });
    57 QObject::connect(buttonPage3, &QPushButton::clicked, [=]() {
    58 layout->setCurrentIndex(2); // 切换到索引为 2 的页面 (Page 3)
    59 });
    60
    61
    62 window.setWindowTitle("QStackedLayout Example");
    63 window.show();
    64
    65 return app.exec();
    66 }

    代码解释:

    QStackedLayout *layout = new QStackedLayout;:创建 QStackedLayout 布局管理器的实例。

    layout->addWidget(page1); layout->addWidget(page2); layout->addWidget(page3);:使用 addWidget() 函数将三个页面控件 (QWidget) 添加到 layout 堆叠布局中。页面控件被堆叠在一起,初始状态只显示索引为 0 的页面 (即 page1)。

    ③ 使用 QObject::connect() 函数连接按钮的 clicked() 信号与 Lambda 槽函数,在槽函数中使用 layout->setCurrentIndex(index) 函数切换当前显示的页面。setCurrentIndex(index) 函数将堆叠布局中索引为 index 的页面设置为当前页面并显示出来。

    常用 API

    addWidget(QWidget *widget):将控件 widget 添加到堆叠布局中。

    insertWidget(int index, QWidget *widget):在指定 索引 位置 index 插入控件 widget

    currentWidget() const:返回当前 显示 的控件。

    currentIndex() const:返回当前 显示 的控件的 索引

    setCurrentWidget(QWidget *widget):将控件 widget 设置为当前 显示 的控件。

    setCurrentIndex(int index):将索引为 index 的控件设置为当前 显示 的控件。

    应用场景

    ⚝ 创建 向导 界面,例如软件安装向导、配置向导等。
    ⚝ 实现 多标签页 效果,例如浏览器标签页、编辑器标签页等。
    ⚝ 构建需要 页面切换 的复杂界面,例如设置界面、多步骤操作界面等。

    4.2.5 布局的嵌套与组合

    在实际的 GUI 开发中,通常需要创建比单一布局管理器能够实现的更复杂的界面布局。这时,就需要使用 布局嵌套布局组合 的技术。

    布局嵌套 是指在一个布局管理器中 添加另一个布局管理器。例如,可以在 QVBoxLayout 中添加多个 QHBoxLayout,或者在 QGridLayout 的某个单元格中放置一个 QVBoxLayout。通过布局嵌套,可以构建出层次分明、结构清晰的复杂布局。

    布局组合 是指将不同的布局管理器 组合 使用,以满足不同的布局需求。例如,可以使用 QHBoxLayout 创建水平工具栏,使用 QVBoxLayout 创建垂直侧边栏,然后将工具栏、侧边栏和主内容区域分别放在一个更大的 QGridLayout 中进行整体布局。

    示例:嵌套布局构建复杂界面

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtWidgets>
    2
    3 int main(int argc, char *argv[]) {
    4 QApplication app(argc, argv);
    5
    6 QWidget window;
    7 QVBoxLayout *mainLayout = new QVBoxLayout; // ① 创建主垂直布局
    8
    9 // 顶部工具栏
    10 QHBoxLayout *toolbarLayout = new QHBoxLayout; // ② 创建工具栏水平布局
    11 QPushButton *buttonOpen = new QPushButton("打开");
    12 QPushButton *buttonSave = new QPushButton("保存");
    13 toolbarLayout->addWidget(buttonOpen);
    14 toolbarLayout->addWidget(buttonSave);
    15 mainLayout->addLayout(toolbarLayout); // ③ 将工具栏布局添加到主布局
    16
    17
    18 // 中间内容区域 (文本编辑框)
    19 QTextEdit *textEdit = new QTextEdit;
    20 mainLayout->addWidget(textEdit); // ④ 将文本编辑框添加到主布局
    21
    22
    23 // 底部状态栏
    24 QHBoxLayout *statusBarLayout = new QHBoxLayout; // ⑤ 创建状态栏水平布局
    25 QLabel *statusLabel = new QLabel("就绪");
    26 statusBarLayout->addWidget(statusLabel);
    27 statusBarLayout->addStretch(); // 添加伸缩空间,使状态标签左对齐
    28 mainLayout->addLayout(statusBarLayout); // ⑥ 将状态栏布局添加到主布局
    29
    30
    31 window.setLayout(mainLayout); // ⑦ 将主布局设置给窗口
    32 window.setWindowTitle("Nested Layout Example");
    33 window.show();
    34
    35 return app.exec();
    36 }

    代码解释:

    ① 创建一个 QVBoxLayout 作为 主布局 (mainLayout),用于垂直排列整个界面的各个部分。

    ② 创建一个 QHBoxLayout 作为 工具栏布局 (toolbarLayout),用于水平排列工具栏按钮。

    ③ 将 toolbarLayout 添加到 mainLayout 中,作为界面的顶部区域。

    ④ 创建一个 QTextEdit 作为 文本编辑区域,并添加到 mainLayout 中,作为界面的中间区域。

    ⑤ 创建一个 QHBoxLayout 作为 状态栏布局 (statusBarLayout),用于水平排列状态栏标签。

    ⑥ 将 statusBarLayout 添加到 mainLayout 中,作为界面的底部区域。

    ⑦ 将 mainLayout 设置给窗口,完成整个界面的布局。

    通过布局嵌套和组合,可以灵活地构建出各种复杂的界面布局,满足不同的应用需求。在实际开发中,开发者需要根据具体的界面设计,合理选择和组合布局管理器,才能创建出既美观又实用的用户界面。

    4.3 尺寸策略 (Size Policies) 与布局微调

    仅仅使用布局管理器,有时还不能完全满足精细的界面设计需求。为了更精确地控制控件在布局中的大小和伸缩行为,Qt 提供了 尺寸策略 (Size Policies) 和一些 布局微调 的方法。

    4.3.1 尺寸策略 (QSizePolicy) 的作用与属性

    尺寸策略 (QSizePolicy) 是 Qt 中用于描述控件在布局中 尺寸调整意愿 的机制。每个控件都拥有一个 QSizePolicy 对象,通过设置 QSizePolicy 对象的属性,可以控制控件在水平和垂直方向上的伸缩、拉伸和压缩行为。

    QSizePolicy 类主要包含以下属性:

    horizontalPolicy (水平尺寸策略):描述控件在 水平方向 上的尺寸策略。
    verticalPolicy (垂直尺寸策略):描述控件在 垂直方向 上的尺寸策略。
    horizontalStretch (水平伸缩因子):水平方向的 伸缩因子,与布局管理器的 stretch 参数类似。
    verticalStretch (垂直伸缩因子):垂直方向的 伸缩因子

    horizontalPolicyverticalPolicy 属性的类型是 QSizePolicy::Policy 枚举,常用的枚举值包括:

    QSizePolicy::Fixed (固定尺寸):控件的尺寸 固定不变,不会随着布局的改变而伸缩。

    QSizePolicy::Minimum (最小尺寸):控件的尺寸可以 拉伸,但不能小于其 最小尺寸 (minimumSizeHint())。

    QSizePolicy::Maximum (最大尺寸):控件的尺寸可以 压缩,但不能大于其 最大尺寸 (maximumSize())。

    QSizePolicy::Preferred (推荐尺寸):控件倾向于使用其 推荐尺寸 (sizeHint()),但也可以根据布局的需要进行 伸缩

    QSizePolicy::Expanding (扩展尺寸):控件可以 尽可能地扩展 以占据所有可用空间。

    QSizePolicy::MinimumExpanding (最小扩展尺寸):控件可以 扩展,但初始尺寸为其 最小尺寸 (minimumSizeHint())。

    QSizePolicy::Ignored (忽略尺寸):控件的尺寸将被 忽略,布局管理器会根据其他控件的尺寸来决定该控件的尺寸。

    设置尺寸策略

    可以通过 QWidget::setSizePolicy(QSizePolicy policy) 函数或 QWidget::setSizePolicy(QSizePolicy::Policy horizontal, QSizePolicy::Policy vertical) 函数来设置控件的尺寸策略。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtWidgets>
    2
    3 int main(int argc, char *argv[]) {
    4 QApplication app(argc, argv);
    5
    6 QWidget window;
    7 QHBoxLayout *layout = new QHBoxLayout;
    8
    9 QPushButton *buttonFixed = new QPushButton("Fixed Size");
    10 buttonFixed->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); // ① 设置为固定尺寸策略
    11
    12 QPushButton *buttonExpanding = new QPushButton("Expanding Size");
    13 buttonExpanding->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // ② 设置为扩展尺寸策略
    14
    15
    16 layout->addWidget(buttonFixed);
    17 layout->addWidget(buttonExpanding);
    18 window.setLayout(layout);
    19 window.setWindowTitle("QSizePolicy Example");
    20 window.show();
    21
    22 return app.exec();
    23 }

    代码解释:

    buttonFixed->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed);:将 buttonFixed 按钮的水平和垂直尺寸策略都设置为 QSizePolicy::Fixed,使其尺寸固定不变。

    buttonExpanding->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);:将 buttonExpanding 按钮的水平和垂直尺寸策略都设置为 QSizePolicy::Expanding,使其尽可能地扩展以占据所有可用空间。

    运行程序并调整窗口大小,可以看到 buttonFixed 按钮的大小始终保持不变,而 buttonExpanding 按钮则会随着窗口大小的变化而伸缩。

    尺寸策略的应用场景

    固定尺寸控件:例如图标按钮、固定大小的标签等,可以设置为 QSizePolicy::Fixed
    需要占据剩余空间的控件:例如文本编辑框、列表视图等,可以设置为 QSizePolicy::ExpandingQSizePolicy::MinimumExpanding
    根据内容自适应大小的控件:例如按钮、标签等,可以设置为 QSizePolicy::Preferred

    4.3.2 最小尺寸 (minimumSize)、最大尺寸 (maximumSize) 与尺寸提示 (sizeHint)

    除了尺寸策略,Qt 还提供了 最小尺寸 (minimumSize)最大尺寸 (maximumSize)尺寸提示 (sizeHint) 等属性,用于更精细地控制控件的尺寸。

    最小尺寸 (minimumSize):控件允许的 最小宽度和高度。控件的尺寸不能小于其最小尺寸。可以通过 QWidget::setMinimumSize(const QSize &size)QWidget::setMinimumWidth(int minw), QWidget::setMinimumHeight(int minh) 函数设置。

    最大尺寸 (maximumSize):控件允许的 最大宽度和高度。控件的尺寸不能大于其最大尺寸。可以通过 QWidget::setMaximumSize(const QSize &size)QWidget::setMaximumWidth(int maxw), QWidget::setMaximumHeight(int maxh) 函数设置。

    尺寸提示 (sizeHint):控件的 推荐尺寸。布局管理器在计算布局时,会参考控件的尺寸提示。默认情况下,Qt 的控件会根据自身的内容和样式计算出一个合适的尺寸提示。可以通过重写 QWidget::sizeHint() 函数自定义控件的尺寸提示。

    示例:设置最小尺寸、最大尺寸和尺寸提示

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtWidgets>
    2
    3 int main(int argc, char *argv[]) {
    4 QApplication app(argc, argv);
    5
    6 QWidget window;
    7 QVBoxLayout *layout = new QVBoxLayout;
    8
    9 QPushButton *button = new QPushButton("Button with Size Limits");
    10
    11 button->setMinimumSize(100, 30); // ① 设置最小尺寸:宽度 100px,高度 30px
    12 button->setMaximumSize(200, 60); // ② 设置最大尺寸:宽度 200px,高度 60px
    13
    14 layout->addWidget(button);
    15 window.setLayout(layout);
    16 window.setWindowTitle("Size Limits Example");
    17 window.show();
    18
    19 return app.exec();
    20 }

    代码解释:

    button->setMinimumSize(100, 30);:设置 button 按钮的最小尺寸为宽度 100 像素,高度 30 像素。按钮的宽度和高度不会小于这个值。

    button->setMaximumSize(200, 60);:设置 button 按钮的最大尺寸为宽度 200 像素,高度 60 像素。按钮的宽度和高度不会大于这个值。

    运行程序并调整窗口大小,可以看到按钮的大小在最小尺寸和最大尺寸之间变化。

    尺寸提示的应用场景

    自定义控件:在自定义控件中,通常需要重写 sizeHint() 函数,返回控件的推荐尺寸,以便布局管理器能够正确地布局控件。
    需要限制尺寸范围的控件:例如进度条、滑动条等,可以通过设置最小尺寸和最大尺寸来限制其尺寸范围。

    4.3.3 布局的间距 (spacing) 与边距 (margin) 设置

    为了使界面布局更加美观和协调,Qt 布局管理器还提供了 间距 (spacing)边距 (margin) 的设置功能。

    间距 (spacing):指 布局管理器内部 控件之间或布局之间 空白间隔。可以通过 QLayout::setSpacing(int spacing) 函数设置布局管理器的间距。

    边距 (margin):指 布局管理器边缘 与其 父控件边缘 之间的 空白距离。可以通过 QLayout::setContentsMargins(int left, int top, int right, int bottom) 函数设置布局管理器的边距。

    示例:设置间距和边距

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtWidgets>
    2
    3 int main(int argc, char *argv[]) {
    4 QApplication app(argc, argv);
    5
    6 QWidget window;
    7 QVBoxLayout *mainLayout = new QVBoxLayout;
    8 mainLayout->setContentsMargins(20, 20, 20, 20); // ① 设置主布局的边距
    9
    10 QHBoxLayout *toolbarLayout = new QHBoxLayout;
    11 toolbarLayout->setSpacing(10); // ② 设置工具栏布局的间距
    12 QPushButton *buttonOpen = new QPushButton("打开");
    13 QPushButton *buttonSave = new QPushButton("保存");
    14 toolbarLayout->addWidget(buttonOpen);
    15 toolbarLayout->addWidget(buttonSave);
    16 mainLayout->addLayout(toolbarLayout);
    17
    18 QTextEdit *textEdit = new QTextEdit;
    19 mainLayout->addWidget(textEdit);
    20
    21 QHBoxLayout *statusBarLayout = new QHBoxLayout;
    22 statusBarLayout->setSpacing(5); // ③ 设置状态栏布局的间距
    23 QLabel *statusLabel = new QLabel("就绪");
    24 statusBarLayout->addWidget(statusLabel);
    25 statusBarLayout->addStretch();
    26 mainLayout->addLayout(statusBarLayout);
    27
    28 window.setLayout(mainLayout);
    29 window.setWindowTitle("Spacing and Margin Example");
    30 window.show();
    31
    32 return app.exec();
    33 }

    代码解释:

    mainLayout->setContentsMargins(20, 20, 20, 20);:设置主布局 mainLayout 的左、上、右、下边距都为 20 像素。这样,主布局的内容与窗口边缘之间就会留出 20 像素的空白。

    toolbarLayout->setSpacing(10);:设置工具栏布局 toolbarLayout 的控件间距为 10 像素。工具栏按钮之间会隔开 10 像素的空白。

    statusBarLayout->setSpacing(5);:设置状态栏布局 statusBarLayout 的控件间距为 5 像素。状态栏标签与伸缩空间之间会隔开 5 像素的空白。

    通过合理设置间距和边距,可以调整控件之间的空隙和布局与窗口边缘的距离,使界面布局更加整洁、美观。

    间距和边距的应用场景

    调整控件之间的距离:通过设置布局管理器的间距,可以控制控件之间的水平或垂直间距,使界面元素之间保持适当的距离。
    控制布局与窗口边缘的距离:通过设置布局管理器的边距,可以控制布局内容与窗口边缘之间的空白距离,避免内容紧贴窗口边缘,提高界面的美观性。
    创建视觉分隔效果:适当的间距和边距可以帮助区分界面上的不同区域,增强界面的层次感和可读性。

    掌握尺寸策略、最小尺寸、最大尺寸、尺寸提示、间距和边距等布局微调技巧,可以帮助开发者更精细地控制 Qt 界面的布局,创建出专业、美观且用户体验良好的 GUI 应用程序。

    5. 事件处理:响应用户交互

    本章深入探讨 Qt 的事件处理机制 (Event Handling Mechanism),讲解如何响应用户的各种操作,例如鼠标点击、键盘输入等。

    5.1 Qt 事件处理机制概述

    本节介绍 Qt 的事件处理机制,包括事件类型、事件循环 (Event Loop)、事件过滤器 (Event Filter) 等。

    5.1.1 事件 (Event) 的类型与分类

    在 GUI 编程中,事件 (Event) 是系统或应用程序发出的信号,表明发生了某些事情。这些事件可以是用户的操作,如鼠标点击、键盘按键,也可以是系统内部的状态变化,例如窗口的创建或销毁。Qt 的事件处理机制是 GUI 应用程序的核心,它允许程序响应用户的交互和系统事件,从而实现动态和交互式的用户界面。

    事件可以根据其来源和性质进行分类。以下是一些常见的事件类型:

    鼠标事件 (Mouse Events):当用户使用鼠标进行操作时产生的事件。
    ▮▮▮▮ⓑ QMouseEvent:鼠标事件是 GUI 交互中最常见的事件类型之一,用于响应鼠标的各种操作。
    ▮▮▮▮▮▮▮▮❸ 鼠标点击事件 (Mouse Click Events)
    QMouseEvent::MouseButtonPress (鼠标按键按下事件)
    QMouseEvent::MouseButtonRelease (鼠标按键释放事件)
    QMouseEvent::MouseButtonDblClick (鼠标双击事件)
    ▮▮▮▮▮▮▮▮❷ 鼠标移动事件 (Mouse Move Events)
    QMouseEvent::MouseMove (鼠标移动事件)
    ▮▮▮▮▮▮▮▮❸ 鼠标滚轮事件 (Mouse Wheel Events)
    QWheelEvent (鼠标滚轮滚动事件)

    键盘事件 (Key Events):当用户按下或释放键盘按键时产生的事件。
    ▮▮▮▮ⓑ QKeyEvent:键盘事件用于响应用户的键盘输入,是文本输入、快捷键操作等功能的基础。
    ▮▮▮▮▮▮▮▮❸ 按键按下事件 (Key Press Events)
    QKeyEvent::KeyPress (按键按下事件)
    ▮▮▮▮▮▮▮▮❷ 按键释放事件 (Key Release Events)
    QKeyEvent::KeyRelease (按键释放事件)

    窗口事件 (Window Events):与窗口状态变化相关的事件。
    ▮▮▮▮ⓑ QWindowEvent:窗口事件反映了窗口的各种状态变化,例如显示、隐藏、激活、关闭、大小调整等。
    ▮▮▮▮▮▮▮▮❸ 显示与隐藏事件 (Show and Hide Events)
    QShowEvent (窗口显示事件)
    QHideEvent (窗口隐藏事件)
    ▮▮▮▮▮▮▮▮❷ 激活与非激活事件 (Activate and Deactivate Events)
    QActivateEvent (窗口激活事件)
    QDeactivateEvent (窗口非激活事件)
    ▮▮▮▮▮▮▮▮❸ 关闭事件 (Close Events)
    QCloseEvent (窗口关闭事件)
    ▮▮▮▮▮▮▮▮❹ 大小调整事件 (Resize Events)
    QResizeEvent (窗口大小调整事件)
    ▮▮▮▮▮▮▮▮❺ 移动事件 (Move Events)
    QMoveEvent (窗口移动事件)
    ▮▮▮▮▮▮▮▮❻ 焦点事件 (Focus Events)
    QFocusInEvent (窗口获得焦点事件)
    QFocusOutEvent (窗口失去焦点事件)

    定时器事件 (Timer Events):由定时器触发的事件。
    ▮▮▮▮ⓑ QTimerEvent:定时器事件允许程序在预定的时间间隔执行特定的任务,常用于动画、周期性数据更新等场景。

    绘制事件 (Paint Events):当控件需要重绘时产生的事件。
    ▮▮▮▮ⓑ QPaintEvent:绘制事件是自定义控件外观的核心,通过处理绘制事件,开发者可以在控件上绘制各种图形和文本。

    自定义事件 (Custom Events):开发者可以自定义事件类型,以满足特定的应用需求。
    ▮▮▮▮ⓑ QEvent:所有事件类的基类。开发者可以继承 QEvent 类创建自定义事件类型,并在应用程序中使用。

    理解不同类型的事件及其触发条件是进行 GUI 编程的基础。Qt 提供了丰富的事件类型,涵盖了用户交互和系统状态变化的各个方面。

    5.1.2 事件循环 (Event Loop) 的工作原理

    事件循环 (Event Loop) 是 Qt GUI 应用程序的核心机制,它负责接收、处理和分发事件。事件循环使 GUI 应用程序能够保持运行状态,并响应用户的操作和系统事件。

    事件循环的工作原理可以用以下步骤概括:

    等待事件 (Wait for Events):应用程序进入事件循环后,会进入等待状态,等待新的事件发生。这个等待通常是通过操作系统提供的机制实现的,例如 selectpollepoll 等系统调用。

    事件获取 (Event Acquisition):当有事件发生时(例如,用户点击鼠标),操作系统会将事件信息传递给应用程序。事件循环会获取这个事件。事件可能来自不同的来源,包括:
    用户输入 (User Input):鼠标、键盘等硬件设备产生的事件。
    系统事件 (System Events):操作系统产生的事件,例如窗口消息、定时器到期等。
    应用程序内部事件 (Internal Events):应用程序自身产生的事件,例如信号 (Signals) 发射、postEvent 函数发布的事件等。

    事件排队 (Event Queuing):获取到的事件会被放入事件队列 (Event Queue) 中。事件队列是一个先进先出 (FIFO, First-In-First-Out) 的数据结构,保证事件按照发生的顺序被处理。

    事件分发 (Event Dispatching):事件循环从事件队列中取出事件,并根据事件的类型和目标对象,将事件分发给相应的事件接收者 (Event Receiver) 进行处理。在 Qt 中,事件接收者通常是 QObject 的子类实例,例如 QWidgetQPushButton 等控件。

    事件处理 (Event Handling):事件接收者接收到事件后,会调用相应的事件处理函数 (Event Handler) 来处理事件。例如,如果事件是鼠标点击事件,接收者可能会调用 mousePressEvent() 函数来响应。事件处理函数中包含了应用程序对事件的具体响应逻辑,例如更新界面、执行计算、发送网络请求等。

    返回等待 (Return to Wait):事件处理完成后,事件循环再次返回等待状态,等待下一个事件的到来。

    这个循环过程持续不断地进行,直到应用程序退出。事件循环保证了 GUI 应用程序能够及时响应各种事件,维持用户界面的交互性和动态性。

    Qt 的事件循环实现 主要由 QCoreApplication::exec() 函数启动。在 main() 函数中,通常会创建 QApplicationQGuiApplication 对象(QCoreApplication 的子类),然后调用 exec() 函数,启动事件循环,开始 GUI 应用程序的生命周期。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QPushButton>
    3
    4 int main(int argc, char *argv[]) {
    5 QApplication app(argc, argv); // 创建 QApplication 对象
    6
    7 QPushButton button("Click me"); // 创建一个按钮
    8 button.show(); // 显示按钮
    9
    10 return app.exec(); // 启动事件循环
    11 }

    在上述代码中,app.exec() 启动了应用程序的事件循环。之后,应用程序会进入事件等待状态,直到用户与按钮进行交互(例如,点击按钮),或者应用程序接收到其他类型的事件。当事件发生时,事件循环会负责将事件分发给相应的对象进行处理。

    5.1.3 事件传播 (Event Propagation):事件的传递路径

    事件传播 (Event Propagation) 描述了事件在 对象树 (Object Tree) 中的传递路径。在 Qt 中,对象以树状结构组织,每个对象都有一个父对象(除了根对象)。当事件发生时,Qt 的事件系统会根据事件的类型和目标对象,决定事件在对象树中的传递路径。

    Qt 的事件传播机制主要有两种方式:

    自顶向下 (Top-Down) 的事件传播:也称为 事件拦截 (Event Interception)。事件首先传递给父对象 (Parent Object),然后由父对象决定是否处理该事件,或者将事件继续传递给子对象 (Child Object)。这种传播方式常用于实现事件过滤器 (Event Filter) 和事件拦截器 (Event Interceptor)。

    ⚝ 例如,当鼠标点击一个子控件时,鼠标点击事件首先会被传递给父窗口,父窗口可以选择拦截这个事件,阻止事件传递到子控件,或者允许事件继续传递到子控件。

    自底向上 (Bottom-Up) 的事件传播:也称为 事件冒泡 (Event Bubbling)。事件首先传递给事件源对象 (Event Source Object) (通常是最底层的子对象),如果事件源对象没有处理该事件,事件会沿着对象树向上逐级传递给父对象 (Parent Object),直到事件被处理或到达根对象。

    ⚝ 例如,当用户在一个嵌套的控件结构中点击最内层的子控件时,点击事件首先会被传递给最内层的子控件。如果最内层子控件没有处理这个事件,事件会传递给它的父控件,然后是父控件的父控件,依此类推,直到事件被某个控件处理或到达顶层窗口。

    Qt 中事件传播的具体流程 通常是:

    确定事件目标 (Determine Event Target):当事件发生时,Qt 事件系统首先确定事件的目标对象,即哪个对象应该接收这个事件。对于用户输入事件(如鼠标点击、键盘按键),目标对象通常是鼠标光标或键盘焦点所在的控件。

    事件发送给目标对象 (Send Event to Target Object):事件系统将事件发送给目标对象进行处理。目标对象会调用相应的事件处理函数来响应事件。

    事件传递给父对象 (Pass Event to Parent Object):如果目标对象的事件处理函数没有 接受 (accept) 该事件(例如,事件处理函数返回 false 或没有调用 event->accept()),事件会沿着对象树向上传递给父对象。

    父对象处理或继续传递 (Parent Object Handles or Continues Passing):父对象接收到事件后,可以选择处理该事件,或者继续将事件传递给它的父对象,直到事件被处理或到达对象树的根部。

    事件未处理 (Event Unhandled):如果事件传播到对象树的根部仍然没有被处理,Qt 事件系统会执行默认的事件处理逻辑(如果有的话)。

    事件的接受与忽略 (Accept and Ignore Events):在事件处理函数中,控件可以通过以下方式控制事件的传播:

    接受事件 (Accept Event):调用 event->accept() 函数,或者事件处理函数返回 true。表示该控件已经处理了该事件,事件传播停止。
    忽略事件 (Ignore Event):调用 event->ignore() 函数,或者事件处理函数返回 false(对于虚函数事件处理函数,默认返回 false)。表示该控件没有处理该事件,事件将继续向父对象传播。

    事件过滤器 (Event Filter):Qt 提供了 事件过滤器 (Event Filter) 机制,允许对象拦截和处理发送给其他对象的事件。通过安装事件过滤器,可以在事件到达目标对象之前,预先处理事件,甚至修改或阻止事件的传递。事件过滤器常用于实现全局事件监听、事件预处理等功能。

    理解事件传播机制对于编写复杂的 GUI 应用程序至关重要。通过控制事件的传播路径,开发者可以灵活地实现各种交互逻辑和事件处理策略。

    5.2 常用事件处理方法

    本节详细讲解常用的事件处理方法,包括重写事件处理函数 (Event Handlers)、使用信号与槽 (Signals and Slots) 连接事件等。

    5.2.1 重写事件处理函数 (Event Handlers):paintEvent(), mousePressEvent(), keyPressEvent()

    重写事件处理函数 (Event Handlers) 是 Qt 中最基本的事件处理方法。对于 QWidget 和其子类,Qt 提供了许多虚函数 (Virtual Functions) 作为事件处理函数。当特定类型的事件发生时,Qt 会调用相应控件的事件处理函数。通过在子类中 重写 (Override) 这些虚函数,可以自定义控件对事件的响应行为。

    常用的事件处理函数包括:

    paintEvent(QPaintEvent *event)绘制事件处理函数。当控件需要重绘时(例如,窗口初次显示、窗口大小改变、调用 update()repaint() 函数),Qt 会调用 paintEvent() 函数。开发者需要在 paintEvent() 函数中使用 QPainter 进行控件的绘制操作。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QPainter>
    3
    4 class MyWidget : public QWidget {
    5 protected:
    6 void paintEvent(QPaintEvent *event) override {
    7 QPainter painter(this); // 创建 QPainter 对象,以当前控件为绘图设备
    8 painter.setBrush(Qt::red); // 设置画刷颜色为红色
    9 painter.drawRect(10, 10, 100, 100); // 绘制一个红色矩形
    10 }
    11 };

    在上述代码中,MyWidget 重写了 paintEvent() 函数,在函数中使用 QPainter 绘制了一个红色的矩形。当 MyWidget 控件需要重绘时,就会执行这段绘制代码。

    mousePressEvent(QMouseEvent *event)鼠标按键按下事件处理函数。当鼠标按键在控件上按下时,Qt 会调用 mousePressEvent() 函数。QMouseEvent 对象包含了鼠标事件的详细信息,例如鼠标按键类型、鼠标位置等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QMouseEvent>
    3 #include <QDebug>
    4
    5 class MyButton : public QWidget {
    6 protected:
    7 void mousePressEvent(QMouseEvent *event) override {
    8 if (event->button() == Qt::LeftButton) { // 判断是否是鼠标左键按下
    9 qDebug() << "Left mouse button pressed at:" << event->pos();
    10 // 处理鼠标左键按下事件
    11 } else if (event->button() == Qt::RightButton) { // 判断是否是鼠标右键按下
    12 qDebug() << "Right mouse button pressed at:" << event->pos();
    13 // 处理鼠标右键按下事件
    14 }
    15 }
    16 };

    在上述代码中,MyButton 重写了 mousePressEvent() 函数,在函数中判断了鼠标按键类型,并输出了鼠标按下的位置信息。

    keyPressEvent(QKeyEvent *event)键盘按键按下事件处理函数。当键盘按键在控件获得焦点时按下时,Qt 会调用 keyPressEvent() 函数。QKeyEvent 对象包含了键盘事件的详细信息,例如按键的键码、修饰键状态等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QKeyEvent>
    3 #include <QDebug>
    4
    5 class MyLineEdit : public QWidget {
    6 protected:
    7 void keyPressEvent(QKeyEvent *event) override {
    8 qDebug() << "Key pressed:" << event->text(); // 输出按键的文本信息
    9 if (event->key() == Qt::Key_Return) { // 判断是否是回车键按下
    10 qDebug() << "Enter key pressed";
    11 // 处理回车键按下事件
    12 } else {
    13 QWidget::keyPressEvent(event); // 调用基类的 keyPressEvent 处理函数,处理默认的键盘输入
    14 }
    15 }
    16 };

    在上述代码中,MyLineEdit 重写了 keyPressEvent() 函数,输出了按键的文本信息,并判断了是否是回车键按下。对于非回车键的按键,调用了基类的 keyPressEvent() 函数,以保证控件能够正常接收键盘输入。

    其他常用的事件处理函数
    mouseReleaseEvent(QMouseEvent *event):鼠标按键释放事件处理函数。
    mouseDoubleClickEvent(QMouseEvent *event):鼠标双击事件处理函数。
    mouseMoveEvent(QMouseEvent *event):鼠标移动事件处理函数。
    wheelEvent(QWheelEvent *event):鼠标滚轮事件处理函数。
    keyReleaseEvent(QKeyEvent *event):键盘按键释放事件处理函数。
    focusInEvent(QFocusEvent *event):控件获得焦点事件处理函数。
    focusOutEvent(QFocusEvent *event):控件失去焦点事件处理函数。
    resizeEvent(QResizeEvent *event):控件大小调整事件处理函数。
    moveEvent(QMoveEvent *event):控件移动事件处理函数。
    closeEvent(QCloseEvent *event):窗口关闭事件处理函数。
    showEvent(QShowEvent *event):控件显示事件处理函数。
    hideEvent(QHideEvent *event):控件隐藏事件处理函数。

    调用基类的事件处理函数:在重写事件处理函数时,通常需要在自定义的事件处理逻辑之后或之前,调用基类的事件处理函数 QWidget::eventHandler(event)。这样做可以保证控件的默认行为不被破坏,例如,对于 keyPressEvent(),如果需要控件能够正常接收键盘输入,就需要调用 QWidget::keyPressEvent(event)

    重写事件处理函数是一种直接且灵活的事件处理方法,适用于自定义控件的行为,或者需要在控件级别精细控制事件响应的场景。

    5.2.2 使用信号与槽 (Signals and Slots) 连接事件:QPushButton::clicked(), QLineEdit::textChanged()

    信号与槽 (Signals and Slots) 是 Qt 框架的核心机制,用于对象间的通信。控件和对象可以发射 信号 (Signals),表示发生了某种事件或状态变化。其他对象可以连接到这些信号,当信号发射时,连接的 槽函数 (Slot Functions) 会被自动调用,从而响应事件。

    对于 Qt 的内置控件,许多常见的用户交互事件都以信号的形式提供。例如:

    QPushButton::clicked()按钮点击信号。当 QPushButton 按钮被点击时,会发射 clicked() 信号。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QPushButton>
    3 #include <QMessageBox>
    4
    5 int main(int argc, char *argv[]) {
    6 QApplication app(argc, argv);
    7
    8 QPushButton button("Click me");
    9
    10 // 定义槽函数,响应按钮点击事件
    11 QObject::connect(&button, &QPushButton::clicked,
    12 []() {
    13 QMessageBox::information(nullptr, "Button Clicked", "Button was clicked!");
    14 });
    15
    16 button.show();
    17 return app.exec();
    18 }

    在上述代码中,QObject::connect() 函数将 button 对象的 clicked() 信号连接到一个 lambda 槽函数。当按钮被点击时,clicked() 信号发射,lambda 槽函数被调用,弹出一个消息框显示 "Button was clicked!"。

    QLineEdit::textChanged(const QString &text)文本框文本改变信号。当 QLineEdit 文本框的文本内容发生改变时,会发射 textChanged(const QString &text) 信号,信号携带新的文本内容。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QLineEdit>
    3 #include <QLabel>
    4 #include <QVBoxLayout>
    5
    6 int main(int argc, char *argv[]) {
    7 QApplication app(argc, argv);
    8
    9 QLineEdit lineEdit;
    10 QLabel label("Text:");
    11
    12 // 定义槽函数,响应文本框文本改变信号
    13 QObject::connect(&lineEdit, &QLineEdit::textChanged,
    14 [&label](const QString &text) {
    15 label.setText("Text: " + text); // 更新标签的文本内容
    16 });
    17
    18 QVBoxLayout layout;
    19 layout.addWidget(&lineEdit);
    20 layout.addWidget(&label);
    21
    22 QWidget window;
    23 window.setLayout(&layout);
    24 window.show();
    25
    26 return app.exec();
    27 }

    在上述代码中,QObject::connect() 函数将 lineEdit 对象的 textChanged() 信号连接到一个 lambda 槽函数。当文本框的文本内容改变时,textChanged() 信号发射,lambda 槽函数被调用,更新 label 标签的文本内容,实时显示文本框的文本。

    其他常用的控件信号
    QPushButton::pressed():按钮按下信号。
    QPushButton::released():按钮释放信号。
    QPushButton::toggled(bool checked):可选中按钮状态切换信号。
    QCheckBox::stateChanged(int state):复选框状态改变信号。
    QRadioButton::toggled(bool checked):单选按钮状态切换信号。
    QComboBox::currentIndexChanged(int index):下拉框当前索引改变信号。
    QListWidget::itemClicked(QListWidgetItem *item):列表框项目被点击信号。
    QSlider::valueChanged(int value):滑动条数值改变信号。
    QSpinBox::valueChanged(int value):微调框数值改变信号。

    使用 QObject::connect() 函数连接信号与槽

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QObject::connect(sender, signal, receiver, slot);

    sender:信号发射者对象指针。
    signal:要连接的信号,使用 &SenderClass::signalName 的形式,例如 &QPushButton::clicked
    receiver:信号接收者对象指针。
    slot:要连接的槽函数,可以使用:
    ▮▮▮▮ⓐ 普通成员函数 (Member Function):使用 &ReceiverClass::slotFunctionName 的形式。
    ▮▮▮▮ⓑ 静态成员函数 (Static Member Function):使用 &ReceiverClass::staticSlotFunctionName 的形式。
    ▮▮▮▮ⓒ 自由函数 (Free Function):使用 slotFunctionName 的函数指针。
    ▮▮▮▮ⓓ Lambda 表达式 (Lambda Expression):直接在 connect 函数中定义匿名函数。

    信号与槽机制的优势

    类型安全 (Type Safety):Qt 的信号与槽机制是类型安全的。连接信号和槽时,编译器会检查信号和槽的参数类型是否匹配,避免类型错误。
    松耦合 (Loose Coupling):信号发射者不需要知道哪个对象连接了它的信号,也不需要知道槽函数的具体实现。信号与槽机制实现了对象之间的松耦合,提高了代码的灵活性和可维护性。
    灵活性 (Flexibility):一个信号可以连接多个槽函数,一个槽函数也可以连接多个信号。信号与槽机制提供了非常灵活的对象间通信方式。

    使用信号与槽连接事件是 Qt 编程中推荐的事件处理方法。它使得代码结构清晰、易于维护,并且充分利用了 Qt 框架的优势。

    5.2.3 事件过滤器 (Event Filter):全局事件监听

    事件过滤器 (Event Filter) 是 Qt 提供的一种强大的事件处理机制,允许一个对象 拦截 (Intercept)预处理 (Pre-process) 发送给另一个对象的所有事件。通过安装事件过滤器,可以在事件到达目标对象之前,对事件进行处理、修改甚至阻止事件的传递。

    事件过滤器的使用步骤

    创建事件过滤器类:创建一个类,继承自 QObject,并重写 eventFilter(QObject *watched, QEvent *event) 函数。eventFilter() 函数是事件过滤器的核心,当被监控的对象接收到事件时,eventFilter() 函数会被调用。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QObject>
    2 #include <QEvent>
    3 #include <QDebug>
    4
    5 class MyEventFilter : public QObject {
    6 public:
    7 MyEventFilter(QObject *parent = nullptr) : QObject(parent) {}
    8
    9 protected:
    10 bool eventFilter(QObject *watched, QEvent *event) override {
    11 if (event->type() == QEvent::MouseButtonPress) { // 过滤鼠标按键按下事件
    12 qDebug() << "Event Filter: Mouse button pressed on" << watched->objectName();
    13 // 可以选择处理事件并阻止事件传递,或者继续传递事件
    14 return false; // 返回 false,继续传递事件给目标对象
    15 } else {
    16 // 对于其他类型的事件,调用基类的 eventFilter 处理函数,保证默认行为
    17 return QObject::eventFilter(watched, event);
    18 }
    19 }
    20 };

    在上述代码中,MyEventFilter 类重写了 eventFilter() 函数,过滤了 QEvent::MouseButtonPress 类型的事件。当被监控的对象接收到鼠标按键按下事件时,eventFilter() 函数会被调用,输出一条调试信息,并返回 false,表示继续将事件传递给目标对象。

    安装事件过滤器:在需要监控事件的对象上,调用 installEventFilter(QObject *filterObj) 函数,将事件过滤器对象安装到该对象上。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2 #include <QPushButton>
    3 #include "myeventfilter.h" // 包含自定义事件过滤器类的头文件
    4
    5 int main(int argc, char *argv[]) {
    6 QApplication app(argc, argv);
    7
    8 QPushButton button("Click me");
    9 button.setObjectName("myButton"); // 设置对象名称,方便在事件过滤器中识别
    10
    11 MyEventFilter *filter = new MyEventFilter(&button); // 创建事件过滤器对象,父对象设置为 button
    12 button.installEventFilter(filter); // 将事件过滤器安装到 button 对象上
    13
    14 button.show();
    15 return app.exec();
    16 }

    在上述代码中,创建了一个 MyEventFilter 对象 filter,并使用 button.installEventFilter(filter) 将事件过滤器安装到 button 按钮上。之后,所有发送给 button 按钮的事件都会先经过 filter 对象的 eventFilter() 函数处理。

    eventFilter(QObject *watched, QEvent *event) 函数的返回值

    true:表示事件已经被事件过滤器处理,事件将 停止传播 (Stop Propagation),不会继续传递给目标对象或其他事件过滤器。
    false:表示事件过滤器没有处理该事件,事件将 继续传播 (Continue Propagation),传递给目标对象或其他事件过滤器进行处理。

    事件过滤器的应用场景

    全局事件监听 (Global Event Monitoring):可以在应用程序的顶层对象(例如 QApplication 或主窗口)上安装事件过滤器,监听整个应用程序的事件,实现全局事件监控和处理。
    事件预处理 (Event Pre-processing):在事件到达目标对象之前,对事件进行预处理,例如修改事件参数、记录事件信息、实现自定义的事件路由策略等。
    事件拦截与阻止 (Event Interception and Blocking):可以根据事件类型和条件,阻止事件传递到目标对象,实现特定的事件控制逻辑。
    简化复杂控件的事件处理 (Simplifying Event Handling for Complex Controls):对于复杂的自定义控件,可以使用事件过滤器将一部分事件处理逻辑分离出来,提高代码的可维护性。

    事件过滤器的注意事项

    性能影响 (Performance Impact):事件过滤器会增加事件处理的开销,特别是当安装了多个事件过滤器或者事件处理逻辑比较复杂时。应谨慎使用事件过滤器,避免过度使用导致性能下降。
    过滤器链 (Filter Chain):一个对象可以安装多个事件过滤器。事件会按照过滤器安装的顺序依次传递给事件过滤器处理。如果某个事件过滤器返回 true,事件传播停止,后续的事件过滤器和目标对象的事件处理函数都不会被调用。
    过滤器作用域 (Filter Scope):事件过滤器只对安装它的对象及其子对象接收到的事件有效。

    事件过滤器是一种高级的事件处理机制,提供了强大的事件拦截和预处理能力,适用于实现复杂的事件管理和控制逻辑。

    5.3 鼠标事件处理

    本节深入讲解鼠标事件的处理,包括鼠标点击、鼠标移动、鼠标滚轮事件等。

    5.3.1 鼠标点击事件:mousePressEvent(), mouseReleaseEvent(), mouseDoubleClickEvent()

    鼠标点击事件 (Mouse Click Events) 是 GUI 交互中最常见的事件类型之一,用于响应用户的鼠标点击操作。Qt 提供了三个主要的鼠标点击事件处理函数:

    mousePressEvent(QMouseEvent *event)鼠标按键按下事件处理函数。当鼠标按键在控件上按下时,Qt 会调用 mousePressEvent() 函数。

    QMouseEvent::button():获取按下的鼠标按键类型,返回值是 Qt::MouseButton 枚举类型,常用的值包括:
    ▮▮▮▮ⓐ Qt::LeftButton:鼠标左键。
    ▮▮▮▮ⓑ Qt::RightButton:鼠标右键。
    ▮▮▮▮ⓒ Qt::MidButton:鼠标中键。
    ▮▮▮▮ⓓ Qt::BackButton:鼠标后退键。
    ▮▮▮▮ⓔ Qt::ForwardButton:鼠标前进键。
    ▮▮▮▮ⓕ Qt::NoButton:没有按键按下。
    QMouseEvent::pos():获取鼠标点击位置相对于控件的坐标,返回值是 QPoint 类型。
    QMouseEvent::globalPos():获取鼠标点击位置相对于屏幕的全局坐标,返回值是 QPoint 类型。
    QMouseEvent::modifiers():获取事件发生时键盘修饰键的状态,返回值是 Qt::KeyboardModifiers 枚举类型,例如 Qt::ShiftModifierQt::ControlModifierQt::AltModifier 等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QMouseEvent>
    3 #include <QDebug>
    4
    5 class ClickableWidget : public QWidget {
    6 protected:
    7 void mousePressEvent(QMouseEvent *event) override {
    8 QString buttonType;
    9 if (event->button() == Qt::LeftButton) {
    10 buttonType = "Left Button";
    11 } else if (event->button() == Qt::RightButton) {
    12 buttonType = "Right Button";
    13 } else if (event->button() == Qt::MidButton) {
    14 buttonType = "Middle Button";
    15 }
    16
    17 QString modifiers;
    18 if (event->modifiers() & Qt::ShiftModifier) {
    19 modifiers += "Shift+";
    20 }
    21 if (event->modifiers() & Qt::ControlModifier) {
    22 modifiers += "Ctrl+";
    23 }
    24 if (event->modifiers() & Qt::AltModifier) {
    25 modifiers += "Alt+";
    26 }
    27
    28 qDebug() << modifiers << buttonType << "Pressed at:" << event->pos();
    29 // 处理鼠标按键按下事件
    30 }
    31 };

    在上述代码中,ClickableWidget 重写了 mousePressEvent() 函数,获取了鼠标按键类型、修饰键状态和点击位置,并输出到调试信息。

    mouseReleaseEvent(QMouseEvent *event)鼠标按键释放事件处理函数。当鼠标按键在控件上释放时,Qt 会调用 mouseReleaseEvent() 函数。QMouseEvent 对象提供的信息与 mousePressEvent() 相同。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QMouseEvent>
    3 #include <QDebug>
    4
    5 class ClickableWidget : public QWidget {
    6 protected:
    7 void mouseReleaseEvent(QMouseEvent *event) override {
    8 QString buttonType;
    9 if (event->button() == Qt::LeftButton) {
    10 buttonType = "Left Button";
    11 } else if (event->button() == Qt::RightButton) {
    12 buttonType = "Right Button";
    13 } else if (event->button() == Qt::MidButton) {
    14 buttonType = "Middle Button";
    15 }
    16 qDebug() << buttonType << "Released at:" << event->pos();
    17 // 处理鼠标按键释放事件
    18 }
    19 };

    在上述代码中,ClickableWidget 重写了 mouseReleaseEvent() 函数,获取了鼠标按键类型和释放位置,并输出到调试信息。

    mouseDoubleClickEvent(QMouseEvent *event)鼠标双击事件处理函数。当鼠标按键在控件上快速连续点击两次时,Qt 会调用 mouseDoubleClickEvent() 函数。QMouseEvent 对象提供的信息与 mousePressEvent()mouseReleaseEvent() 相同。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QMouseEvent>
    3 #include <QDebug>
    4
    5 class ClickableWidget : public QWidget {
    6 protected:
    7 void mouseDoubleClickEvent(QMouseEvent *event) override {
    8 QString buttonType;
    9 if (event->button() == Qt::LeftButton) {
    10 buttonType = "Left Button";
    11 } else if (event->button() == Qt::RightButton) {
    12 buttonType = "Right Button";
    13 } else if (event->button() == Qt::MidButton) {
    14 buttonType = "Middle Button";
    15 }
    16 qDebug() << buttonType << "Double Clicked at:" << event->pos();
    17 // 处理鼠标双击事件
    18 }
    19 };

    在上述代码中,ClickableWidget 重写了 mouseDoubleClickEvent() 函数,获取了鼠标按键类型和双击位置,并输出到调试信息。

    区分鼠标点击类型:通过重写 mousePressEvent(), mouseReleaseEvent(), mouseDoubleClickEvent() 函数,可以分别响应鼠标按下、释放和双击事件。在事件处理函数中,可以通过 QMouseEvent::button() 函数判断是哪个鼠标按键触发了事件,从而实现不同的响应逻辑。

    鼠标事件的顺序:一次完整的鼠标点击操作通常会产生以下事件序列:

    按下 (Press)mousePressEvent()
    释放 (Release)mouseReleaseEvent()
    点击 (Click):如果按下和释放之间的时间间隔和鼠标移动距离足够小,会认为发生了一次点击。
    双击 (Double Click):如果两次点击之间的时间间隔足够短,会认为发生了双击,触发 mouseDoubleClickEvent()

    注意:默认情况下,为了提高性能,控件可能不会跟踪鼠标移动事件。如果需要处理鼠标移动事件,需要启用 鼠标跟踪 (Mouse Tracking) 功能,通过调用 setMouseTracking(true) 函数来开启。

    5.3.2 鼠标移动事件:mouseMoveEvent(), 鼠标跟踪 (Mouse Tracking)

    鼠标移动事件 (Mouse Move Events) 用于响应鼠标在控件上移动的操作。Qt 提供了 mouseMoveEvent(QMouseEvent *event) 函数处理鼠标移动事件。

    mouseMoveEvent(QMouseEvent *event)鼠标移动事件处理函数。当鼠标在控件上移动时,Qt 会调用 mouseMoveEvent() 函数。

    QMouseEvent::pos():获取鼠标当前位置相对于控件的坐标。
    QMouseEvent::globalPos():获取鼠标当前位置相对于屏幕的全局坐标。
    QMouseEvent::buttons():获取当前处于按下状态的鼠标按键,返回值是 Qt::MouseButtons 枚举类型,可以同时包含多个按键状态,例如 Qt::LeftButton | Qt::RightButton 表示左右键同时按下。
    QMouseEvent::modifiers():获取事件发生时键盘修饰键的状态。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QMouseEvent>
    3 #include <QDebug>
    4
    5 class MovableWidget : public QWidget {
    6 public:
    7 MovableWidget(QWidget *parent = nullptr) : QWidget(parent) {
    8 setMouseTracking(true); // 启用鼠标跟踪
    9 }
    10
    11 protected:
    12 void mouseMoveEvent(QMouseEvent *event) override {
    13 qDebug() << "Mouse moved to:" << event->pos();
    14 // 处理鼠标移动事件,例如更新控件显示、实现拖拽功能等
    15 }
    16 };

    在上述代码中,MovableWidget 启用了鼠标跟踪功能 setMouseTracking(true),并重写了 mouseMoveEvent() 函数,输出鼠标移动的位置信息。

    鼠标跟踪 (Mouse Tracking):默认情况下,为了提高性能,Qt 控件只在鼠标按键按下时才接收鼠标移动事件。如果需要控件在鼠标移动时(即使没有按键按下)也接收鼠标移动事件,需要启用 鼠标跟踪 (Mouse Tracking) 功能。

    setMouseTracking(bool enable):设置是否启用鼠标跟踪。enabletrue 时,启用鼠标跟踪;enablefalse 时,禁用鼠标跟踪(默认值)。
    ⚝ 启用鼠标跟踪后,当鼠标在控件上移动时,即使没有按键按下,也会持续触发 mouseMoveEvent() 事件。
    ⚝ 禁用鼠标跟踪时(默认情况),只有在鼠标按键按下并移动鼠标时,才会触发 mouseMoveEvent() 事件。

    鼠标移动事件的应用

    鼠标悬停效果 (Mouse Hover Effects):通过监听鼠标移动事件,可以实现鼠标悬停在控件上时的视觉效果,例如高亮显示、 ToolTip 提示等。
    拖拽操作 (Drag and Drop):鼠标移动事件是实现拖拽操作的基础。在 mousePressEvent() 中记录拖拽开始位置,在 mouseMoveEvent() 中根据鼠标移动距离更新拖拽对象的位置。
    自定义绘制 (Custom Painting):根据鼠标移动轨迹进行自定义绘制,例如画笔工具、手绘板等应用。
    实时信息显示 (Real-time Information Display):根据鼠标位置实时显示信息,例如状态栏提示、坐标显示等。

    5.3.3 鼠标滚轮事件:wheelEvent()

    鼠标滚轮事件 (Mouse Wheel Events) 用于响应鼠标滚轮的滚动操作。Qt 提供了 wheelEvent(QWheelEvent *event) 函数处理鼠标滚轮事件。

    wheelEvent(QWheelEvent *event)鼠标滚轮事件处理函数。当鼠标滚轮滚动时,Qt 会调用 wheelEvent() 函数。

    QWheelEvent::angleDelta():获取滚轮滚动的角度增量,返回值是 QPoint 类型。通常,垂直滚动时,y 分量非零;水平滚动时,x 分量非零。角度单位是 1/8 度,例如,标准的鼠标滚轮一格滚动角度是 15 度,angleDelta().y() 返回值是 15 * 8 = 120。
    QWheelEvent::pixelDelta():获取滚轮滚动的像素增量,返回值是 QPoint 类型。对于高精度鼠标滚轮,可能会返回像素增量,而不是角度增量。如果 pixelDelta() 返回值非零,应优先使用像素增量。
    QWheelEvent::modifiers():获取事件发生时键盘修饰键的状态。
    QWheelEvent::orientation():获取滚轮滚动方向,返回值是 Qt::Orientation 枚举类型,Qt::Vertical 表示垂直滚动,Qt::Horizontal 表示水平滚动。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QWheelEvent>
    3 #include <QDebug>
    4
    5 class WheelWidget : public QWidget {
    6 protected:
    7 void wheelEvent(QWheelEvent *event) override {
    8 QPoint numDegrees = event->angleDelta() / 8; // 将角度增量转换为度数
    9 QPoint numSteps = numDegrees / 15; // 标准鼠标滚轮每格滚动 15 度
    10
    11 qDebug() << "Wheel scrolled:";
    12 if (!numSteps.isNull()) {
    13 qDebug() << "Steps:" << numSteps; // 输出步进值
    14 }
    15 if (!event->pixelDelta().isNull()) {
    16 qDebug() << "Pixels:" << event->pixelDelta(); // 输出像素增量
    17 }
    18 qDebug() << "Orientation:" << (event->orientation() == Qt::Vertical ? "Vertical" : "Horizontal"); // 输出滚动方向
    19
    20 // 处理鼠标滚轮事件,例如滚动内容、缩放视图等
    21 }
    22 };

    在上述代码中,WheelWidget 重写了 wheelEvent() 函数,获取了滚轮滚动角度增量、像素增量和滚动方向,并输出到调试信息。

    鼠标滚轮事件的应用

    内容滚动 (Content Scrolling):鼠标滚轮最常见的应用是滚动显示内容,例如滚动文本框、滚动列表、滚动网页等。
    视图缩放 (View Zooming):在图形视图、地图应用、图像浏览器等场景中,鼠标滚轮常用于缩放视图的比例。
    数值调节 (Value Adjustment):在一些数值调节控件中,鼠标滚轮可以用来微调数值,例如音量调节、亮度调节等。
    自定义交互 (Custom Interaction):根据滚轮滚动方向和步进值,实现自定义的交互逻辑,例如切换标签页、调整动画速度等。

    滚轮滚动方向和步进值:通过 QWheelEvent::angleDelta()QWheelEvent::pixelDelta() 函数,可以获取滚轮滚动的方向和步进值,从而根据滚轮操作进行相应的响应。对于标准的鼠标滚轮,通常使用角度增量 angleDelta() 即可;对于高精度鼠标滚轮,可能需要优先使用像素增量 pixelDelta()

    5.4 键盘事件处理

    本节深入讲解键盘事件的处理,包括按键按下、按键释放事件等,以及如何获取按键信息。

    5.4.1 按键按下事件:keyPressEvent(), 按键修饰符 (Modifiers)

    按键按下事件 (Key Press Events) 用于响应用户按下键盘按键的操作。Qt 提供了 keyPressEvent(QKeyEvent *event) 函数处理按键按下事件。

    keyPressEvent(QKeyEvent *event)按键按下事件处理函数。当键盘按键在控件获得焦点时按下时,Qt 会调用 keyPressEvent() 函数。

    QKeyEvent::key():获取按下的按键的 键码 (Key Code),返回值是 int 类型,通常使用 Qt::Key 枚举类型进行比较,例如 Qt::Key_AQt::Key_ReturnQt::Key_Space 等。
    QKeyEvent::text():获取按键输入的 文本 (Text),返回值是 QString 类型。对于字符按键,返回输入的字符;对于功能键,可能返回空字符串。
    QKeyEvent::modifiers():获取事件发生时键盘 修饰键 (Modifiers) 的状态,返回值是 Qt::KeyboardModifiers 枚举类型,常用的修饰键包括:
    ▮▮▮▮ⓐ Qt::ShiftModifier:Shift 键。
    ▮▮▮▮ⓑ Qt::ControlModifier:Ctrl 键(在 macOS 上是 Command 键)。
    ▮▮▮▮ⓒ Qt::AltModifier:Alt 键(在 macOS 上是 Option 键)。
    ▮▮▮▮ⓓ Qt::MetaModifier:Meta 键(在 Windows 键或 macOS Command 键)。
    ▮▮▮▮ⓔ Qt::KeypadModifier:数字键盘键。
    ▮▮▮▮ⓕ Qt::GroupSwitchModifier:Group Switch 键。
    QKeyEvent::isAutoRepeat():判断是否是 自动重复按键 (Auto-repeat Key)。当按住按键不放时,键盘会持续发送按键按下事件,这些事件是自动重复按键事件。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QKeyEvent>
    3 #include <QDebug>
    4
    5 class KeyPressWidget : public QWidget {
    6 protected:
    7 void keyPressEvent(QKeyEvent *event) override {
    8 qDebug() << "Key Pressed:";
    9 qDebug() << " Key Code:" << event->key(); // 输出键码
    10 qDebug() << " Text:" << event->text(); // 输出文本
    11 qDebug() << " Auto-repeat:" << event->isAutoRepeat(); // 输出是否是自动重复按键
    12
    13 QString modifiers;
    14 if (event->modifiers() & Qt::ShiftModifier) {
    15 modifiers += "Shift+";
    16 }
    17 if (event->modifiers() & Qt::ControlModifier) {
    18 modifiers += "Ctrl+";
    19 }
    20 if (event->modifiers() & Qt::AltModifier) {
    21 modifiers += "Alt+";
    22 }
    23 qDebug() << " Modifiers:" << modifiers; // 输出修饰键状态
    24
    25 if (event->key() == Qt::Key_Escape) { // 判断是否是 Esc 键按下
    26 qDebug() << "Escape key pressed, closing widget";
    27 close(); // 关闭控件
    28 } else if (event->key() == Qt::Key_Return || event->key() == Qt::Key_Enter) { // 判断是否是回车键按下
    29 qDebug() << "Enter key pressed";
    30 // 处理回车键事件
    31 }
    32 }
    33 };

    在上述代码中,KeyPressWidget 重写了 keyPressEvent() 函数,输出了按键的键码、文本、是否是自动重复按键以及修饰键状态。并对 Esc 键和回车键进行了特殊处理。

    按键修饰符 (Modifiers)修饰键 (Modifier Keys) 是指 Shift、Ctrl、Alt 等按键,它们通常与其他按键组合使用,改变其他按键的功能。通过 QKeyEvent::modifiers() 函数可以获取事件发生时修饰键的状态,使用位运算 & 可以判断某个修饰键是否被按下。例如:event->modifiers() & Qt::ShiftModifier 判断 Shift 键是否按下。

    按键按下事件的应用

    快捷键 (Shortcuts):通过监听按键按下事件,并结合修饰键状态,可以实现应用程序的快捷键功能。例如,Ctrl+S 保存、Ctrl+C 复制等。
    文本输入 (Text Input):对于文本输入控件(如 QLineEditQTextEdit),按键按下事件是接收用户键盘输入的基础。
    游戏控制 (Game Controls):在游戏应用中,键盘按键常用于控制游戏角色的移动、操作等。
    自定义交互逻辑 (Custom Interaction Logic):根据按下的按键类型和修饰键状态,实现自定义的交互逻辑。

    5.4.2 按键释放事件:keyReleaseEvent()

    按键释放事件 (Key Release Events) 用于响应用户释放键盘按键的操作。Qt 提供了 keyReleaseEvent(QKeyEvent *event) 函数处理按键释放事件。

    keyReleaseEvent(QKeyEvent *event)按键释放事件处理函数。当键盘按键被释放时,Qt 会调用 keyReleaseEvent() 函数。QKeyEvent 对象提供的信息与 keyPressEvent() 相同,包括键码、文本、修饰键状态等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QKeyEvent>
    3 #include <QDebug>
    4
    5 class KeyReleaseWidget : public QWidget {
    6 protected:
    7 void keyReleaseEvent(QKeyEvent *event) override {
    8 qDebug() << "Key Released:";
    9 qDebug() << " Key Code:" << event->key(); // 输出键码
    10 qDebug() << " Text:" << event->text(); // 输出文本
    11
    12 QString modifiers;
    13 if (event->modifiers() & Qt::ShiftModifier) {
    14 modifiers += "Shift+";
    15 }
    16 if (event->modifiers() & Qt::ControlModifier) {
    17 modifiers += "Ctrl+";
    18 }
    19 if (event->modifiers() & Qt::AltModifier) {
    20 modifiers += "Alt+";
    21 }
    22 qDebug() << " Modifiers:" << modifiers; // 输出修饰键状态
    23
    24 // 处理按键释放事件
    25 }
    26 };

    在上述代码中,KeyReleaseWidget 重写了 keyReleaseEvent() 函数,输出了按键的键码、文本和修饰键状态。

    按键释放事件的应用

    组合按键检测 (Combination Key Detection):在一些场景下,需要检测组合按键是否被释放完成,例如,在游戏中,可能需要检测方向键和 Shift 键同时释放时,停止角色奔跑。
    状态切换 (State Switching):按键释放事件可以用于触发状态切换,例如,按住某个按键进行某种操作,释放按键后停止操作。
    性能优化 (Performance Optimization):对于一些需要持续执行的操作(例如,动画、连续移动),可以在按键按下事件中启动操作,在按键释放事件中停止操作,避免在自动重复按键事件中重复启动操作,提高性能。

    按键按下与释放事件的配对:一次完整的按键操作通常会产生 按键按下 (KeyPress)按键释放 (KeyRelease) 两个事件,它们是配对出现的。在处理键盘事件时,可以同时监听 keyPressEvent()keyReleaseEvent() 函数,实现更丰富的键盘交互逻辑。

    5.4.3 键盘焦点 (Keyboard Focus) 管理

    键盘焦点 (Keyboard Focus) 指的是当前正在接收键盘输入的控件。在 GUI 应用程序中,只有一个控件能够拥有键盘焦点。拥有键盘焦点的控件会接收键盘事件,响应用户的键盘输入。

    焦点管理的相关概念

    获得焦点 (Focus In):当一个控件 获得键盘焦点 (Gain Focus) 时,会触发 focusInEvent(QFocusEvent *event) 事件。Qt 会调用控件的 focusInEvent() 函数。

    QFocusEvent::reason():获取焦点改变的原因,返回值是 Qt::FocusReason 枚举类型,例如:
    ▮▮▮▮ⓐ Qt::MouseFocusReason:鼠标点击导致焦点改变。
    ▮▮▮▮ⓑ Qt::TabFocusReason:Tab 键切换焦点导致焦点改变。
    ▮▮▮▮ⓒ Qt::BacktabFocusReason:Shift+Tab 键切换焦点导致焦点改变。
    ▮▮▮▮ⓓ Qt::ActiveWindowFocusReason:窗口激活导致焦点改变。
    ▮▮▮▮ⓔ Qt::PopupFocusReason:弹出窗口显示导致焦点改变。
    ▮▮▮▮ⓕ Qt::OtherFocusReason:其他原因导致焦点改变。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QFocusEvent>
    3 #include <QDebug>
    4
    5 class FocusWidget : public QWidget {
    6 protected:
    7 void focusInEvent(QFocusEvent *event) override {
    8 QString reason;
    9 switch (event->reason()) {
    10 case Qt::MouseFocusReason: reason = "Mouse Click"; break;
    11 case Qt::TabFocusReason: reason = "Tab Key"; break;
    12 case Qt::BacktabFocusReason: reason = "Shift+Tab Key"; break;
    13 case Qt::ActiveWindowFocusReason: reason = "Window Active"; break;
    14 case Qt::PopupFocusReason: reason = "Popup Window"; break;
    15 case Qt::OtherFocusReason: reason = "Other"; break;
    16 default: reason = "Unknown"; break;
    17 }
    18 qDebug() << "Focus In, Reason:" << reason;
    19 // 处理获得焦点事件
    20 }
    21 };

    在上述代码中,FocusWidget 重写了 focusInEvent() 函数,输出了焦点改变的原因。

    失去焦点 (Focus Out):当一个控件 失去键盘焦点 (Lose Focus) 时,会触发 focusOutEvent(QFocusEvent *event) 事件。Qt 会调用控件的 focusOutEvent() 函数。QFocusEvent::reason() 函数提供的信息与 focusInEvent() 相同。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QFocusEvent>
    3 #include <QDebug>
    4
    5 class FocusWidget : public QWidget {
    6 protected:
    7 void focusOutEvent(QFocusEvent *event) override {
    8 QString reason;
    9 switch (event->reason()) {
    10 case Qt::MouseFocusReason: reason = "Mouse Click"; break;
    11 case Qt::TabFocusReason: reason = "Tab Key"; break;
    12 case Qt::BacktabFocusReason: reason = "Shift+Tab Key"; break;
    13 case Qt::ActiveWindowFocusReason: reason = "Window Active"; break;
    14 case Qt::PopupFocusReason: reason = "Popup Window"; break;
    15 case Qt::OtherFocusReason: reason = "Other"; break;
    16 default: reason = "Unknown"; break;
    17 }
    18 qDebug() << "Focus Out, Reason:" << reason;
    19 // 处理失去焦点事件
    20 }
    21 };

    在上述代码中,FocusWidget 重写了 focusOutEvent() 函数,输出了焦点改变的原因。

    设置焦点 (Set Focus):可以使用以下函数手动设置控件的键盘焦点:

    QWidget::setFocus(Qt::FocusReason reason):将键盘焦点设置到当前控件。reason 参数指定焦点改变的原因,可以是 Qt::MouseFocusReasonQt::TabFocusReason 等。
    QWidget::clearFocus():清除当前控件的键盘焦点,将焦点移开。
    QWidget::focusPolicy()QWidget::setFocusPolicy(Qt::FocusPolicy policy):获取和设置控件的 焦点策略 (Focus Policy),决定控件如何以及是否能够获得焦点。Qt::FocusPolicy 枚举类型定义了不同的焦点策略,例如:
    ▮▮▮▮ⓐ Qt::NoFocus:控件不能获得焦点。
    ▮▮▮▮ⓑ Qt::TabFocus:可以通过 Tab 键获得焦点。
    ▮▮▮▮ⓒ Qt::ClickFocus:可以通过鼠标点击获得焦点。
    ▮▮▮▮ⓓ Qt::StrongFocus:可以通过 Tab 键和鼠标点击获得焦点(默认值)。
    ▮▮▮▮ⓔ Qt::WheelFocus:可以通过鼠标滚轮滚动获得焦点。
    ▮▮▮▮ⓕ Qt::WheelFocus:可以通过鼠标滚轮滚动获得焦点。
    ▮▮▮▮ⓖ Qt::FocusPolicy::ClickAndFocusStyle:可以通过鼠标点击获得焦点,并且具有焦点样式。
    ▮▮▮▮ⓗ Qt::FocusPolicy::TabAndClickFocusStyle:可以通过 Tab 键和鼠标点击获得焦点,并且具有焦点样式。
    ▮▮▮▮ⓘ Qt::FocusPolicy::StrongFocusAndFocusStyle:可以通过 Tab 键和鼠标点击获得焦点,并且具有焦点样式。
    ▮▮▮▮ⓙ Qt::FocusPolicy::WheelFocusAndFocusStyle:可以通过鼠标滚轮滚动获得焦点,并且具有焦点样式。

    焦点代理 (Focus Proxy):可以使用 QWidget::setFocusProxy(QWidget *proxy) 函数为一个控件设置 焦点代理 (Focus Proxy)。当用户尝试将焦点设置到该控件时,焦点实际上会被设置到焦点代理控件上。焦点代理常用于组合控件,例如,一个包含标签和文本框的自定义控件,可以将焦点代理设置为文本框,使得用户点击标签时,焦点能够自动转移到文本框。

    键盘焦点管理的重要性

    用户交互 (User Interaction):键盘焦点决定了哪个控件接收键盘输入,是实现键盘交互的核心。合理的焦点管理能够引导用户操作,提高用户体验。
    可访问性 (Accessibility):对于需要键盘操作的用户,良好的焦点管理是保证应用程序可访问性的重要因素。用户可以使用 Tab 键、方向键等在控件之间切换焦点,完成应用程序的操作。
    控制流程 (Control Flow):焦点管理可以控制应用程序的控制流程。例如,在表单输入界面,可以使用焦点顺序控制用户输入的顺序。
    视觉反馈 (Visual Feedback):当控件获得焦点时,通常会显示 焦点指示器 (Focus Indicator),例如虚线框、高亮边框等,向用户明确当前焦点所在位置,提供视觉反馈。

    焦点切换的顺序 (Tab Order):在对话框和窗口中,控件的焦点切换顺序 (Tab Order) 决定了用户按下 Tab 键时焦点在控件之间的移动顺序。默认情况下,焦点顺序按照控件在窗口中创建的顺序排列。可以使用以下函数自定义焦点顺序:

    QWidget::setTabOrder(QWidget *first, QWidget *second):设置 first 控件之后 Tab 键的焦点移动到 second 控件。可以使用多次 setTabOrder() 调用设置完整的焦点顺序。

    总结:键盘焦点管理是 GUI 编程中不可或缺的一部分。合理的焦点策略、焦点顺序和焦点事件处理,能够提升用户交互体验,增强应用程序的可访问性和可用性。

    6. 图形与绘制:自定义界面元素

    本章介绍 Qt 的图形绘制系统 (Graphics and Drawing System),讲解如何进行 2D 图形绘制,以及如何自定义界面元素。

    6.1 Qt 绘图系统 (Painting System) 概述

    介绍 Qt 的绘图系统,包括绘图设备 (Paint Device)、画家 (Painter)、画笔 (Pen)、画刷 (Brush)、字体 (Font) 等基本概念。

    6.1.1 绘图设备 (Paint Device):QWidget, QImage, QPixmap, QPicture

    介绍常用的绘图设备 (Paint Device),如 QWidget, QImage, QPixmap, QPicture,以及它们的特点和用途。

    在 Qt 的图形绘制框架中,绘图设备 (Paint Device) 是所有绘制操作的目标。你可以将绘图设备想象成一块画布,画家 (QPainter) 在这块画布上进行绘制。Qt 提供了多种绘图设备,每种设备都有其特定的用途和特性:

    QWidget: QWidget 是 GUI 应用程序中最常见的绘图设备。任何继承自 QWidget 的类,例如窗口 (Window)、按钮 (Button)、标签 (Label) 等,都可以作为绘图设备。在 QWidget 上绘制的内容会直接显示在用户界面上。QWidget 是屏幕绘图的主要设备,它与屏幕显示紧密结合,能够响应用户的交互事件。

    QImage: QImage 提供了与硬件无关的图像表示方式,它在内存中以像素数据的形式存储图像。QImage 支持多种图像格式,并且可以方便地进行像素级别的操作。QImage 主要用于图像处理离屏渲染 (Off-screen Rendering)。例如,你可以使用 QImage 加载图像文件,对图像进行滤镜处理,然后再将处理后的图像显示在 QWidget 上。QImage 允许你直接访问和修改像素数据,因此非常灵活。

    QPixmap: QPixmap 是为在屏幕上高效显示图像而优化的绘图设备。它通常使用原生窗口系统的绘图接口,因此在屏幕显示方面性能更佳。QPixmap 适用于加载和显示图像文件,特别是作为控件的背景或图标。与 QImage 不同,QPixmap 通常不直接用于像素级别的操作,因为它更多地是针对显示优化,而非图像处理。QPixmap 可以更高效地利用硬件加速进行图像渲染。

    QPicture: QPicture 是一种矢量绘图设备,它将一系列的 QPainter 指令记录下来,形成一个可重放的绘图过程。QPicture 的优点在于其与分辨率无关,可以无损地缩放。当你需要存储和重用一系列复杂的绘图指令,或者需要在不同分辨率下保持图像质量时,QPicture 非常有用。例如,你可以将一个复杂的自定义图标绘制过程记录为 QPicture,然后在不同的控件或场景中重用它。QPicture 本身不显示任何内容,它需要通过 QPainter 在其他绘图设备上“播放”才能显示出来。

    总结来说,这四种绘图设备各有侧重:

    QWidget: 用于直接在用户界面上绘制,响应用户交互。
    QImage: 用于图像处理、像素操作和离屏渲染,功能强大,但可能不如 QPixmap 在屏幕显示上高效。
    QPixmap: 用于高效地在屏幕上显示图像,特别是静态图像,利用硬件加速。
    QPicture: 用于记录和重放矢量绘图指令,与分辨率无关,适合存储和重用复杂绘图过程。

    在实际应用中,通常会根据具体需求选择合适的绘图设备。例如,在自定义控件的绘制中,QWidget 是最常用的设备;在进行图像编辑或处理时,QImage 是更好的选择;当需要显示图标或背景图像时,QPixmap 更高效;而对于需要存储和重用复杂矢量图形时,QPicture 则非常适合。

    6.1.2 画家 (QPainter):绘图操作的核心类

    详细讲解 QPainter 类,它是进行绘图操作的核心类。

    QPainter 是 Qt 绘图系统的核心类,它提供了丰富的 API,用于在各种绘图设备 (Paint Device) 上执行 2D 图形绘制操作。你可以把 QPainter 想象成一个真正的画家,它使用画笔 (QPen)、画刷 (QBrush)、字体 (QFont) 等工具,在画布(绘图设备)上绘制各种图形、文本和图像。

    QPainter 的主要功能包括:

    开始和结束绘制: 任何绘制操作都必须在 QPainter::begin()QPainter::end() 函数之间进行。begin() 函数接受一个绘图设备作为参数,指定 QPainter 在哪个设备上进行绘制。end() 函数结束绘制操作,释放 QPainter 占用的资源。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyWidget::paintEvent(QPaintEvent *event)
    2 {
    3 QPainter painter(this); // 'this' is a QWidget, acting as the paint device
    4 painter.begin(this);
    5
    6 // Drawing operations will be placed here
    7
    8 painter.end();
    9 }

    设置绘图属性: QPainter 允许你设置各种绘图属性,例如:

    画笔 (QPen): 用于绘制轮廓线,可以设置颜色、线宽、线型 (实线、虚线等)、线端样式、连接样式等。通过 QPainter::setPen() 函数设置当前画笔。
    画刷 (QBrush): 用于填充封闭图形的内部区域,可以设置颜色、填充模式 (纯色、渐变、纹理等)。通过 QPainter::setBrush() 函数设置当前画刷。
    字体 (QFont): 用于绘制文本,可以设置字体族、字号、字重、斜体、下划线等。通过 QPainter::setFont() 函数设置当前字体。
    合成模式 (Composition Mode): 控制源图形如何与目标图形混合。通过 QPainter::setCompositionMode() 函数设置。
    抗锯齿 (Antialiasing): 使绘制的图形边缘更平滑。可以通过 QPainter::setRenderHint(QPainter::Antialiasing) 启用。

    绘制各种图形: QPainter 提供了绘制各种基本 2D 图形的函数,例如:

    drawLine(): 绘制直线。
    drawRect(): 绘制矩形。
    drawEllipse(): 绘制椭圆或圆。
    drawArc(): 绘制圆弧。
    drawPie(): 绘制扇形。
    drawChord(): 绘制弦形。
    drawPolygon(): 绘制多边形。
    drawPolyline(): 绘制折线。
    drawPoints(): 绘制一系列点。
    drawText(): 绘制文本。
    drawImage() / drawPixmap() / drawPicture(): 绘制图像或图片。

    坐标系统变换: QPainter 支持坐标系统的变换,允许你进行平移 (translate)、旋转 (rotate)、缩放 (scale)、错切 (shear) 等操作。坐标变换可以简化复杂图形的绘制,并实现各种视觉效果。

    translate(): 平移坐标原点。
    rotate(): 旋转坐标系统。
    scale(): 缩放坐标系统。
    shear(): 错切坐标系统。
    setWorldTransform() / worldTransform(): 直接设置或获取世界变换矩阵。
    resetTransform(): 重置所有变换。

    状态保存与恢复: QPainter 允许你保存当前的绘图状态 (包括画笔、画刷、字体、变换等),并在之后恢复到保存的状态。这在绘制复杂图形时非常有用,可以避免频繁地设置绘图属性。

    save(): 保存当前绘图状态。
    restore(): 恢复到最近一次保存的绘图状态。

    使用 QPainter 的基本流程通常如下:

    1. 在绘图设备 (如 QWidget 的 paintEvent()) 中,创建一个 QPainter 对象,并将绘图设备作为参数传递给 QPainter 的构造函数。
    2. 调用 QPainter::begin() 函数开始绘制。
    3. 设置绘图属性,例如画笔、画刷、字体等。
    4. 调用各种 draw...() 函数进行图形绘制。
    5. 可以进行坐标系统变换,绘制更复杂的图形。
    6. 使用 save()restore() 保存和恢复绘图状态。
    7. 调用 QPainter::end() 函数结束绘制。

    QPainter 是一个功能强大且灵活的类,掌握 QPainter 的使用是进行 Qt GUI 编程,特别是自定义界面元素绘制的关键。

    6.1.3 画笔 (QPen)、画刷 (QBrush)、字体 (QFont):绘图属性设置

    介绍画笔 (QPen)、画刷 (QBrush) 和字体 (QFont) 等绘图属性的设置方法。

    在 Qt 的绘图系统中,画笔 (QPen)、画刷 (QBrush) 和字体 (QFont) 是 QPainter 用来控制绘制外观的关键工具。它们决定了线条的样式、填充的模式以及文本的呈现方式。

    ① 画笔 (QPen)

    QPen 用于绘制轮廓线,例如图形的边框、线条、曲线等。你可以通过 QPen 设置线条的各种属性:

    颜色 (Color): 使用 QPen::setColor() 设置线条颜色,颜色可以使用 QColor 类来表示。
    宽度 (Width): 使用 QPen::setWidthF()QPen::setWidth() 设置线条宽度,单位可以是像素或浮点数。
    线型 (Style): 使用 QPen::setStyle() 设置线条样式,常见的样式包括:
    ▮▮▮▮⚝ Qt::SolidLine: 实线
    ▮▮▮▮⚝ Qt::DashLine: 虚线
    ▮▮▮▮⚝ Qt::DotLine: 点线
    ▮▮▮▮⚝ Qt::DashDotLine: 划线点线
    ▮▮▮▮⚝ Qt::DashDotDotLine: 划线点点线
    ▮▮▮▮⚝ Qt::NoPen: 无轮廓线
    线端样式 (CapStyle): 使用 QPen::setCapStyle() 设置线条末端的样式,例如:
    ▮▮▮▮⚝ Qt::FlatCap: 平直线端
    ▮▮▮▮⚝ Qt::SquareCap: 方形线端
    ▮▮▮▮⚝ Qt::RoundCap: 圆形线端
    连接样式 (JoinStyle): 使用 QPen::setJoinStyle() 设置线条连接处的样式,例如:
    ▮▮▮▮⚝ Qt::MiterJoin: 尖角连接
    ▮▮▮▮⚝ Qt::BevelJoin: 斜角连接
    ▮▮▮▮⚝ Qt::RoundJoin: 圆角连接

    创建和使用 QPen 的示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QPen pen;
    3
    4 // 设置颜色为红色
    5 pen.setColor(Qt::red);
    6 // 设置线宽为 3 像素
    7 pen.setWidth(3);
    8 // 设置线型为虚线
    9 pen.setStyle(Qt::DashLine);
    10 // 设置线端样式为圆形
    11 pen.setCapStyle(Qt::RoundCap);
    12 // 设置连接样式为圆角
    13 pen.setJoinStyle(Qt::RoundJoin);
    14
    15 painter.setPen(pen); // 设置 QPainter 使用该画笔
    16 painter.drawRect(50, 50, 100, 100); // 绘制一个红色虚线边框的矩形

    ② 画刷 (QBrush)

    QBrush 用于填充封闭图形的内部区域,例如矩形、椭圆、多边形等。你可以通过 QBrush 设置填充的各种属性:

    颜色 (Color): 使用 QBrush::setColor() 设置填充颜色,同样可以使用 QColor 类。
    填充模式 (Style): 使用 QBrush::setStyle() 设置填充模式,常见的模式包括:
    ▮▮▮▮⚝ Qt::SolidPattern: 纯色填充
    ▮▮▮▮⚝ Qt::Dense1PatternQt::Dense7Pattern: 不同密度的点状填充
    ▮▮▮▮⚝ Qt::HorPattern: 水平线填充
    ▮▮▮▮⚝ Qt::VerPattern: 垂直线填充
    ▮▮▮▮⚝ Qt::CrossPattern: 交叉线填充
    ▮▮▮▮⚝ Qt::DiagFwdPattern: 前斜线填充
    ▮▮▮▮⚝ Qt::DiagBwdPattern: 后斜线填充
    ▮▮▮▮⚝ Qt::LinearGradientPattern: 线性渐变填充
    ▮▮▮▮⚝ Qt::RadialGradientPattern: 径向渐变填充
    ▮▮▮▮⚝ Qt::ConicalGradientPattern: 锥形渐变填充
    ▮▮▮▮⚝ Qt::TexturePattern: 纹理填充 (使用 QPixmap 作为纹理)
    ▮▮▮▮⚝ Qt::NoBrush: 无填充

    对于渐变填充,你需要使用 QLinearGradient, QRadialGradient, QConicalGradient 类来创建渐变对象,并将其设置为画刷的填充模式。

    创建和使用 QBrush 的示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QBrush brush;
    3
    4 // 设置颜色为蓝色
    5 brush.setColor(Qt::blue);
    6 // 设置填充模式为实心
    7 brush.setStyle(Qt::SolidPattern);
    8
    9 painter.setBrush(brush); // 设置 QPainter 使用该画刷
    10 painter.drawEllipse(200, 50, 100, 100); // 绘制一个蓝色实心填充的椭圆
    11
    12
    13 // 创建一个线性渐变画刷
    14 QLinearGradient linearGradient(QPointF(250, 50), QPointF(350, 150));
    15 linearGradient.setColorAt(0.0, Qt::yellow);
    16 linearGradient.setColorAt(1.0, Qt::green);
    17 QBrush gradientBrush(linearGradient);
    18 painter.setBrush(gradientBrush);
    19 painter.drawRect(200, 200, 100, 100); // 绘制一个线性渐变填充的矩形

    ③ 字体 (QFont)

    QFont 用于设置文本绘制的字体属性。你可以通过 QFont 设置字体的各种属性:

    字体族 (Family): 使用 QFont::setFamily() 设置字体族名称,例如 "Arial", "Times New Roman", "Helvetica" 等。
    字号 (Point Size / Pixel Size): 使用 QFont::setPointSize() (点) 或 QFont::setPixelSize() (像素) 设置字号大小。
    字重 (Weight): 使用 QFont::setWeight() 设置字体的粗细,例如 QFont::Light, QFont::Normal, QFont::Bold 等。
    斜体 (Italic): 使用 QFont::setItalic() 设置是否为斜体。
    下划线 (Underline): 使用 QFont::setUnderline() 设置是否添加下划线。
    删除线 (Strikeout): 使用 QFont::setStrikeOut() 设置是否添加删除线。

    创建和使用 QFont 的示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QFont font;
    3
    4 // 设置字体族为 Arial
    5 font.setFamily("Arial");
    6 // 设置字号为 20 点
    7 font.setPointSize(20);
    8 // 设置字重为粗体
    9 font.setWeight(QFont::Bold);
    10 // 设置为斜体
    11 font.setItalic(true);
    12
    13 painter.setFont(font); // 设置 QPainter 使用该字体
    14 painter.drawText(50, 350, "Hello, Qt!"); // 绘制文本

    通过灵活地组合和设置 QPen, QBrush, QFont,你可以控制绘制图形和文本的各种外观属性,创建丰富多彩的用户界面元素。在实际编程中,通常会根据不同的绘制需求,创建和配置不同的画笔、画刷和字体对象,并将它们应用到 QPainter 上。

    6.1.4 坐标系统 (Coordinate System) 与变换 (Transformation)

    讲解 Qt 的坐标系统 (Coordinate System),以及如何进行坐标变换 (Transformation),如平移、旋转、缩放等。

    Qt 使用笛卡尔坐标系统 (Cartesian Coordinate System) 进行 2D 绘图。默认情况下,QWidget 的坐标系统原点 (0, 0) 位于左上角,X 轴正方向向右,Y 轴正方向向下。坐标单位是像素。

    坐标系统 是一个参考框架,用于定位和描述图形元素的位置。在 Qt 中,每个绘图设备都有自己的坐标系统。QWidget 的坐标系统是相对于其自身左上角而言的。

    坐标变换 (Transformation) 是指对坐标系统进行数学变换,从而改变图形在绘图设备上的位置、大小和方向。QPainter 提供了多种坐标变换函数,可以实现平移、旋转、缩放、错切等效果。

    ① 平移 (Translation)

    平移变换将坐标系统原点移动到新的位置。使用 QPainter::translate(dx, dy) 函数进行平移,其中 dxdy 分别是 X 轴和 Y 轴方向的平移量。平移后,后续的绘制操作将相对于新的原点进行。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2
    3 painter.drawRect(50, 50, 100, 100); // 绘制一个矩形,位置在 (50, 50)
    4
    5 painter.translate(150, 0); // 将坐标原点向右平移 150 像素
    6 painter.drawRect(50, 50, 100, 100); // 再次绘制一个矩形,位置在 (50+150, 50) = (200, 50)

    ② 旋转 (Rotation)

    旋转变换使坐标系统绕原点旋转一定的角度。使用 QPainter::rotate(angle) 函数进行旋转,其中 angle 是旋转角度,单位是度 (degrees),正值表示逆时针旋转,负值表示顺时针旋转。旋转后,后续的绘制操作将相对于旋转后的坐标系统进行。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2
    3 painter.drawRect(50, 50, 100, 100); // 绘制一个矩形
    4
    5 painter.translate(100, 100); // 将原点平移到矩形中心附近
    6 painter.rotate(45); // 绕新原点逆时针旋转 45 度
    7 painter.drawRect(-50, -50, 100, 100); // 绘制一个矩形,由于坐标系旋转,矩形也会旋转

    ③ 缩放 (Scaling)

    缩放变换改变坐标系统的单位长度,从而放大或缩小图形。使用 QPainter::scale(sx, sy) 函数进行缩放,其中 sxsy 分别是 X 轴和 Y 轴方向的缩放比例。缩放比例大于 1 表示放大,小于 1 表示缩小。缩放后,后续的绘制操作将相对于缩放后的坐标系统进行。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2
    3 painter.drawRect(50, 50, 100, 100); // 绘制一个矩形
    4
    5 painter.scale(0.5, 0.5); // 将坐标系统缩小为原来的一半
    6 painter.drawRect(50, 50, 100, 100); // 再次绘制一个矩形,由于坐标系缩小,矩形也会缩小

    ④ 错切 (Shearing)

    错切变换使矩形变成平行四边形,产生倾斜效果。使用 QPainter::shear(sh, sv) 函数进行错切,其中 shsv 分别是 X 轴和 Y 轴方向的错切系数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2
    3 painter.drawRect(50, 50, 100, 100); // 绘制一个矩形
    4
    5 painter.shear(0.5, 0); // 水平方向错切
    6 painter.drawRect(50, 50, 100, 100); // 绘制一个水平错切的矩形

    ⑤ 变换矩阵 (Transformation Matrix)

    所有的坐标变换都可以用一个 3x3 的变换矩阵 (Transformation Matrix) 来表示。QPainter 内部使用 QTransform 类来管理变换矩阵。你可以使用 QPainter::transform() 函数获取当前的变换矩阵,也可以使用 QPainter::setTransform() 函数直接设置变换矩阵。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QTransform transform;
    3
    4 transform.translate(100, 100); // 平移
    5 transform.rotate(30); // 旋转
    6 transform.scale(0.8, 0.8); // 缩放
    7
    8 painter.setTransform(transform); // 设置变换矩阵
    9 painter.drawRect(0, 0, 100, 100); // 绘制的矩形将应用上述所有变换

    ⑥ 状态保存与恢复

    坐标变换是累积的,每次调用变换函数都会在前一次变换的基础上进行。为了避免变换的累积效应影响后续的绘制操作,可以使用 QPainter::save()QPainter::restore() 函数来保存和恢复坐标系统的状态。save() 函数保存当前的变换矩阵,restore() 函数将坐标系统恢复到最近一次保存的状态。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2
    3 painter.drawRect(50, 50, 100, 100);
    4
    5 painter.save(); // 保存当前状态
    6 painter.translate(150, 0);
    7 painter.rotate(30);
    8 painter.drawRect(50, 50, 100, 100);
    9 painter.restore(); // 恢复到保存的状态,即平移和旋转被撤销
    10
    11 painter.drawRect(300, 50, 100, 100); // 绘制的矩形将不受平移和旋转的影响

    坐标系统变换是 Qt 绘图系统中非常重要的概念,它可以帮助你灵活地控制图形的位置、大小和方向,简化复杂图形的绘制,并实现各种动画和特效。熟练掌握坐标变换技巧,可以让你在自定义界面元素绘制方面更加得心应手。

    6.2 常用 2D 图形绘制

    讲解如何使用 QPainter 绘制常用的 2D 图形,例如直线、矩形、椭圆、多边形、文本等。

    6.2.1 绘制基本图形:drawLine(), drawRect(), drawEllipse(), drawPolygon()

    讲解如何使用 QPainter 的函数绘制基本图形,如直线、矩形、椭圆和多边形。

    QPainter 提供了丰富的函数用于绘制各种基本的 2D 图形。以下介绍 drawLine(), drawRect(), drawEllipse(), drawPolygon() 等常用函数的用法。

    ① 绘制直线:drawLine()

    drawLine() 函数用于绘制直线段。它有多种重载形式,最常用的形式接受两个 QPointF 或 QPoint 对象作为参数,表示直线的起点和终点。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void QPainter::drawLine(const QPointF &p1, const QPointF &p2)
    2 void QPainter::drawLine(const QPoint &p1, const QPoint &p2)
    3 void QPainter::drawLine(qreal x1, qreal y1, qreal x2, qreal y2)
    4 void QPainter::drawLine(int x1, int y1, int x2, int y2)

    示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QPen pen;
    3 pen.setWidth(2);
    4 painter.setPen(pen);
    5
    6 // 使用 QPointF 对象指定起点和终点
    7 painter.drawLine(QPointF(50, 50), QPointF(150, 100));
    8
    9 // 使用坐标值直接指定起点和终点
    10 painter.drawLine(200, 50, 300, 100);

    ② 绘制矩形:drawRect()

    drawRect() 函数用于绘制矩形。它也有多种重载形式,可以接受QRectF, QRect 对象或坐标值来定义矩形的位置和尺寸。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void QPainter::drawRect(const QRectF &rect)
    2 void QPainter::drawRect(const QRect &rect)
    3 void QPainter::drawRect(qreal x, qreal y, qreal width, qreal height)
    4 void QPainter::drawRect(int x, int y, int width, int height)

    示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QPen pen;
    3 pen.setColor(Qt::blue);
    4 painter.setPen(pen);
    5 QBrush brush(Qt::yellow);
    6 painter.setBrush(brush);
    7
    8 // 使用 QRectF 对象定义矩形
    9 QRectF rect1(50, 50, 100, 80);
    10 painter.drawRect(rect1);
    11
    12 // 使用坐标值直接定义矩形
    13 painter.drawRect(200, 50, 100, 80);

    ③ 绘制椭圆或圆:drawEllipse()

    drawEllipse() 函数用于绘制椭圆或圆。当矩形的宽度和高度相等时,绘制的就是圆。drawEllipse() 函数同样可以接受 QRectF, QRect 对象或坐标值来定义椭圆的外接矩形。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void QPainter::drawEllipse(const QRectF &rect)
    2 void QPainter::drawEllipse(const QRect &rect)
    3 void QPainter::drawEllipse(const QPointF &center, qreal rx, qreal ry)
    4 void QPainter::drawEllipse(const QPoint &center, int rx, int ry)
    5 void QPainter::drawEllipse(qreal x, qreal y, qreal width, qreal height)
    6 void QPainter::drawEllipse(int x, int y, int width, int height)

    示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QPen pen;
    3 pen.setColor(Qt::green);
    4 pen.setWidth(3);
    5 painter.setPen(pen);
    6 QBrush brush(Qt::NoBrush); // 无填充
    7 painter.setBrush(brush);
    8
    9 // 使用 QRectF 对象定义椭圆的外接矩形
    10 QRectF ellipseRect(50, 50, 100, 60);
    11 painter.drawEllipse(ellipseRect);
    12
    13 // 使用中心点和半径定义椭圆
    14 painter.drawEllipse(QPointF(250, 80), 50, 30);
    15
    16 // 绘制圆 (宽度和高度相等)
    17 painter.drawEllipse(350, 50, 60, 60);

    ④ 绘制多边形:drawPolygon()

    drawPolygon() 函数用于绘制多边形。你需要提供一个 QPolygonF 或 QPolygon 对象,其中包含多边形各个顶点的坐标。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void QPainter::drawPolygon(const QPolygonF &points, Qt::FillRule fillRule = Qt::OddEvenFill)
    2 void QPainter::drawPolygon(const QPolygon &points, Qt::FillRule fillRule = Qt::OddEvenFill)

    fillRule 参数指定多边形的填充规则,常用的值有 Qt::OddEvenFill (奇偶填充) 和 Qt::WindingFill (非零缠绕数填充)。

    示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QPen pen;
    3 pen.setColor(Qt::magenta);
    4 painter.setPen(pen);
    5 QBrush brush(Qt::cyan);
    6 painter.setBrush(brush);
    7
    8 // 定义多边形的顶点
    9 QPolygonF polygon;
    10 polygon << QPointF(50, 50) << QPointF(100, 30) << QPointF(150, 50) << QPointF(120, 100) << QPointF(80, 100);
    11
    12 painter.drawPolygon(polygon);

    除了上述基本图形,QPainter 还提供了绘制圆弧 (drawArc()), 扇形 (drawPie()), 弦形 (drawChord()), 折线 (drawPolyline()), 点 (drawPoints()) 等函数。通过组合这些基本图形绘制函数,可以构建出各种复杂的 2D 图形。在实际应用中,你可以根据需要选择合适的函数,并配合画笔、画刷、坐标变换等技巧,绘制出满足需求的图形元素。

    6.2.2 绘制文本:drawText(),字体设置与排版

    介绍如何使用 QPainter 绘制文本,以及字体设置和文本排版。

    drawText() 函数是 QPainter 中用于绘制文本的核心函数。它可以将字符串绘制到指定的区域或位置,并支持字体设置和文本排版。

    drawText() 函数有多种重载形式,常用的包括:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void QPainter::drawText(const QPointF &point, const QString &text)
    2 void QPainter::drawText(const QPoint &point, const QString &text)
    3 void QPainter::drawText(qreal x, qreal y, const QString &text)
    4 void QPainter::drawText(int x, int y, const QString &text)
    5 void QPainter::drawText(const QRectF &rect, int flags, const QString &text, QRectF *boundingRect = nullptr)
    6 void QPainter::drawText(const QRect &rect, int flags, const QString &text, QRect *boundingRect = nullptr)

    ① 基本文本绘制

    最简单的用法是指定文本的左下角坐标 (point) 和要绘制的字符串 (text)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QFont font;
    3 font.setPointSize(24);
    4 painter.setFont(font);
    5
    6 painter.drawText(QPointF(50, 50), "Hello, Qt Text!"); // 文本左下角位于 (50, 50)

    ② 字体设置

    在绘制文本之前,需要使用 QPainter::setFont() 函数设置字体。字体可以使用 QFont 类进行配置,包括字体族、字号、字重、斜体等属性。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QFont font;
    3 font.setFamily("Times New Roman");
    4 font.setPointSize(30);
    5 font.setBold(true);
    6 font.setItalic(true);
    7 painter.setFont(font);
    8
    9 painter.drawText(QPointF(50, 100), "Styled Text Example");

    ③ 文本对齐与排版

    drawText() 函数还支持在指定的矩形区域内绘制文本,并进行对齐和排版。这需要使用 drawText(const QRectF &rect, int flags, const QString &text) 形式,其中 rect 参数指定文本绘制的矩形区域,flags 参数是 Qt::TextFlag 枚举值的组合,用于控制文本的对齐方式和排版行为。

    常用的 flags 包括:

    水平对齐:
    ▮▮▮▮⚝ Qt::AlignLeft: 左对齐
    ▮▮▮▮⚝ Qt::AlignRight: 右对齐
    ▮▮▮▮⚝ Qt::AlignHCenter: 水平居中
    ▮▮▮▮⚝ Qt::AlignJustify: 对齐 (两端对齐,仅对包含空格的文本有效)
    垂直对齐:
    ▮▮▮▮⚝ Qt::AlignTop: 顶部对齐
    ▮▮▮▮⚝ Qt::AlignBottom: 底部对齐
    ▮▮▮▮⚝ Qt::AlignVCenter: 垂直居中
    ▮▮▮▮⚝ Qt::AlignBaseline: 基线对齐 (默认)
    文本排版:
    ▮▮▮▮⚝ Qt::TextWordWrap: 自动换行 (按单词换行)
    ▮▮▮▮⚝ Qt::TextWrapAnywhere: 强制换行 (可在任意字符处换行)
    ▮▮▮▮⚝ Qt::TextDontClip: 不裁剪超出矩形区域的文本
    ▮▮▮▮⚝ Qt::TextShowMnemonic: 显示助记符 (例如 '&' 字符)
    ▮▮▮▮⚝ Qt::TextSingleLine: 单行显示,超出矩形区域则省略
    ▮▮▮▮⚝ Qt::TextExpandTabs: 扩展制表符为空格

    可以使用 按位或 (|) 运算符组合多个 flags

    示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QFont font;
    3 font.setPointSize(18);
    4 painter.setFont(font);
    5
    6 QRectF textRect(50, 150, 300, 100);
    7 painter.setPen(Qt::darkGray);
    8 painter.drawRect(textRect); // 绘制文本区域的边框
    9
    10 int flags = Qt::AlignCenter | Qt::TextWordWrap; // 水平垂直居中,单词换行
    11 painter.setPen(Qt::black);
    12 painter.drawText(textRect, flags, "This is a long text example to demonstrate text alignment and word wrapping within a specified rectangle.");

    ④ 获取文本边界矩形

    drawText() 函数的 boundingRect 参数 (可选) 可以返回实际绘制文本的边界矩形。这对于文本排版和布局非常有用。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QString text = "Measure this text";
    3 QRectF boundingRectangle;
    4 painter.drawText(QPointF(50, 300), text, &boundingRectangle);
    5
    6 painter.setPen(Qt::red);
    7 painter.drawRect(boundingRectangle.translated(50, 300)); // 绘制文本的边界矩形

    通过 drawText() 函数,结合字体设置和文本排版标志,可以实现各种复杂的文本绘制效果。在 GUI 应用程序中,文本绘制是不可或缺的一部分,例如控件上的标签、按钮上的文字、自定义绘制的文本信息等,都需要使用 drawText() 函数来完成。

    6.2.3 绘制路径 (QPainterPath):复杂图形的构建

    讲解 QPainterPath 类 (QPainterPath),用于构建复杂的图形路径。

    QPainterPath 类是 Qt 绘图系统中用于创建和存储复杂图形路径的类。一个 QPainterPath 对象可以由直线段、曲线段、弧线段等多种图形元素组成,可以表示非常复杂的形状。QPainterPath 可以被填充、描边,也可以作为裁剪区域使用。

    QPainterPath 的主要特点和用途:

    构建复杂形状: QPainterPath 允许你通过一系列的添加操作,逐步构建出复杂的图形轮廓,例如贝塞尔曲线、圆角矩形、组合图形等。
    路径重用: 创建好的 QPainterPath 对象可以被多次绘制,而无需重复构建路径,提高绘图效率。
    填充和描边: QPainterPath 可以被 QPainter 的 fillPath()strokePath() 函数分别进行填充和描边。
    裁剪区域: QPainterPath 可以作为 QPainter 的裁剪路径,限制绘制区域的形状。
    几何运算: QPainterPath 支持各种几何运算,例如路径的合并、相交、差集、异或等,可以创建更复杂的组合形状。

    QPainterPath 的常用操作:

    创建 QPainterPath 对象:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainterPath path;

    移动到起始点:moveTo()

    moveTo(x, y)moveTo(const QPointF &point) 函数用于设置路径的起始点,相当于提起画笔,移动到指定位置。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 path.moveTo(50, 50); // 设置起始点为 (50, 50)

    添加线段:lineTo()

    lineTo(x, y)lineTo(const QPointF &point) 函数用于从当前点到指定点添加一条直线段

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 path.lineTo(150, 50); // 从 (50, 50) 到 (150, 50) 添加一条水平线段

    添加曲线段:cubicTo(), quadTo()

    cubicTo(controlPoint1, controlPoint2, endPoint): 添加三次贝塞尔曲线,需要两个控制点和一个终点。
    quadTo(controlPoint, endPoint): 添加二次贝塞尔曲线,需要一个控制点和一个终点。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 添加三次贝塞尔曲线
    2 path.cubicTo(QPointF(80, 0), QPointF(120, 100), QPointF(150, 50));
    3
    4 // 添加二次贝塞尔曲线
    5 path.quadTo(QPointF(100, 100), QPointF(150, 150));

    添加弧线段:arcTo()

    arcTo() 函数用于添加圆弧或椭圆弧线段。它有多种重载形式,可以指定外接矩形、起始角度、跨越角度等参数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 添加圆弧,外接矩形为 (50, 50, 100, 100),起始角度 0 度,跨越角度 90 度
    2 path.arcTo(QRectF(50, 50, 100, 100), 0, 90);

    添加矩形、椭圆:addRect(), addEllipse()

    addRect()addEllipse() 函数可以直接向路径中添加矩形或椭圆子路径。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 path.addRect(QRectF(200, 50, 100, 80)); // 添加矩形
    2 path.addEllipse(QRectF(350, 50, 80, 80)); // 添加椭圆

    闭合子路径:closeSubpath()

    closeSubpath() 函数用于闭合当前子路径,即将当前点与子路径的起始点连接起来,形成一个封闭的图形。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 path.moveTo(50, 200);
    2 path.lineTo(100, 200);
    3 path.lineTo(100, 250);
    4 path.closeSubpath(); // 闭合子路径,形成三角形

    绘制路径:drawPath(), fillPath(), strokePath()

    QPainter::drawPath(const QPainterPath &path): 绘制路径的轮廓线 (使用当前画笔)。
    QPainter::fillPath(const QPainterPath &path, const QBrush &brush): 填充路径内部 (使用指定的画刷)。
    QPainter::strokePath(const QPainterPath &path, const QPen &pen): 绘制路径的轮廓线 (使用指定的画笔,忽略 QPainter 当前的画笔)。

    示例:绘制一个心形

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QPainterPath heartPath;
    3
    4 heartPath.moveTo(100, 150);
    5 heartPath.cubicTo(QPointF(50, 50), QPointF(50, 0), QPointF(100, 0));
    6 heartPath.cubicTo(QPointF(150, 0), QPointF(150, 50), QPointF(100, 150));
    7 heartPath.closeSubpath();
    8
    9 painter.setBrush(Qt::red);
    10 painter.setPen(Qt::NoPen);
    11 painter.fillPath(heartPath, QBrush(Qt::red)); // 填充红色
    12
    13 painter.setPen(QPen(Qt::black, 2));
    14 painter.setBrush(Qt::NoBrush);
    15 painter.drawPath(heartPath); // 绘制黑色轮廓线

    QPainterPath 是构建复杂自定义图形的强大工具。通过组合各种路径元素,并利用几何运算,可以创建出各种富有创意和表现力的界面元素。在需要绘制复杂图标、自定义形状控件或实现特殊视觉效果时,QPainterPath 将会发挥重要作用。

    6.2.4 填充与裁剪 (Fill and Clip)

    介绍如何使用画刷填充图形,以及如何使用裁剪区域限制绘制范围。

    ① 填充 (Fill)

    填充是指用指定的颜色、图案或渐变色填充封闭图形的内部区域。在 QPainter 中,填充操作通常与画刷 (QBrush) 配合使用。

    设置画刷:QPainter::setBrush(const QBrush &brush)

    在绘制需要填充的图形之前,需要先使用 QPainter::setBrush() 函数设置当前使用的画刷。画刷定义了填充的颜色和模式。

    绘制填充图形:drawRect(), drawEllipse(), drawPolygon(), fillPath()

    当调用 drawRect(), drawEllipse(), drawPolygon() 等函数绘制封闭图形时,QPainter 会自动使用当前设置的画刷来填充图形的内部区域。对于 QPainterPath,可以使用 fillPath() 函数进行填充。

    示例:填充矩形和椭圆

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2
    3 // 填充红色矩形
    4 painter.setBrush(QBrush(Qt::red));
    5 painter.setPen(Qt::NoPen); // 无轮廓线
    6 painter.drawRect(50, 50, 100, 80);
    7
    8 // 填充蓝色渐变椭圆
    9 QRadialGradient gradient(QPointF(300, 90), 50, QPointF(300, 50));
    10 gradient.setColorAt(0.0, Qt::white);
    11 gradient.setColorAt(1.0, Qt::blue);
    12 painter.setBrush(QBrush(gradient));
    13 painter.setPen(QPen(Qt::darkBlue, 2)); // 蓝色轮廓线
    14 painter.drawEllipse(250, 50, 100, 80);

    ② 裁剪 (Clip)

    裁剪是指限制绘制操作的有效区域,只有位于裁剪区域内的图形部分才会被绘制出来,超出裁剪区域的部分会被忽略。Qt 提供了两种裁剪方式:矩形裁剪路径裁剪

    矩形裁剪:QPainter::setClipRect(), QPainter::setClipRegion()

    ▮▮▮▮⚝ QPainter::setClipRect(const QRectF &rect, Qt::ClipOperation operation = Qt::ReplaceClip): 设置矩形裁剪区域。
    ▮▮▮▮⚝ QPainter::setClipRegion(const QRegion &region, Qt::ClipOperation operation = Qt::ReplaceClip): 设置任意形状的区域裁剪。

    operation 参数指定裁剪操作的方式,常用的值有:
    ▮▮▮▮⚝ Qt::ReplaceClip: 替换当前的裁剪区域为新的区域 (默认)。
    ▮▮▮▮⚝ Qt::IntersectClip: 将当前的裁剪区域与新的区域求交集,得到新的裁剪区域。

    路径裁剪:QPainter::setClipPath()

    ▮▮▮▮⚝ QPainter::setClipPath(const QPainterPath &path, Qt::ClipOperation operation = Qt::ReplaceClip): 使用 QPainterPath 对象设置裁剪路径。

    清除裁剪:QPainter::setClipping(false)

    ▮▮▮▮⚝ QPainter::setClipping(false): 禁用裁剪,恢复到整个绘图设备区域。

    示例:矩形裁剪

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2
    3 painter.setBrush(QBrush(Qt::green));
    4 painter.setPen(Qt::NoPen);
    5
    6 // 设置矩形裁剪区域
    7 painter.setClipRect(QRectF(50, 50, 100, 100));
    8
    9 // 绘制多个矩形,只有位于裁剪区域内的部分会被绘制
    10 for (int i = 0; i < 5; ++i) {
    11 painter.drawRect(i * 30, i * 30, 80, 80);
    12 }
    13
    14 painter.setClipping(false); // 清除裁剪
    15 painter.setPen(QPen(Qt::red, 2));
    16 painter.drawRect(50, 50, 100, 100); // 绘制裁剪区域的边框

    示例:路径裁剪

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPainter painter(this);
    2 QPainterPath clipPath;
    3 clipPath.addEllipse(QRectF(250, 50, 100, 100)); // 创建圆形裁剪路径
    4
    5 painter.setClipPath(clipPath); // 设置路径裁剪
    6
    7 painter.setBrush(QBrush(Qt::yellow));
    8 painter.setPen(Qt::NoPen);
    9 painter.drawRect(200, 0, 200, 200); // 绘制一个超出裁剪区域的矩形,只有圆形区域内的部分会被绘制
    10
    11 painter.setClipping(false); // 清除裁剪
    12 painter.setPen(QPen(Qt::blue, 2));
    13 painter.drawPath(clipPath); // 绘制裁剪路径的边框

    填充和裁剪是图形绘制中常用的技巧。填充可以为图形添加颜色和纹理,使界面元素更加丰富多彩;裁剪可以限制绘制区域,实现特殊的视觉效果,例如遮罩、剪切等。合理运用填充和裁剪,可以提升 GUI 界面的美观性和表现力。

    6.3 图像处理与显示

    介绍如何加载、处理和显示图像,以及如何进行简单的图像操作。

    6.3.1 加载图像:QImage, QPixmap

    讲解如何使用 QImage 和 QPixmap 加载图像文件。

    Qt 提供了 QImage 和 QPixmap 两个类用于处理图像。它们都可以用于加载图像文件,但侧重点略有不同。

    QImage: 主要用于图像处理像素级别的操作。它可以加载各种图像格式,并提供访问和修改像素数据的接口。
    QPixmap: 主要用于屏幕显示,针对显示优化,性能更高。通常用于加载和显示静态图像,作为控件的背景或图标。

    ① 使用 QImage 加载图像

    QImage 提供了多种静态函数用于加载图像文件,最常用的是 QImage::load() 函数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QImage QImage::load(const QString &fileName, const char *format = nullptr)

    fileName: 图像文件的路径。
    format: 图像格式,如果为 nullptr (默认),Qt 会尝试根据文件内容自动检测格式。可以显式指定格式,例如 "PNG", "JPEG", "BMP" 等。

    QImage::load() 函数返回一个 QImage 对象,如果加载失败,则返回空图像。

    示例:使用 QImage 加载 PNG 和 JPEG 图像

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QImage pngImage;
    2 if (pngImage.load(":/images/logo.png")) { // 从资源文件加载 PNG 图像
    3 // 加载成功,可以进行后续操作
    4 qDebug() << "PNG image loaded successfully.";
    5 } else {
    6 qDebug() << "Failed to load PNG image.";
    7 }
    8
    9 QImage jpegImage;
    10 if (jpegImage.load("image.jpg", "JPEG")) { // 从本地文件加载 JPEG 图像,并显式指定格式
    11 // 加载成功
    12 qDebug() << "JPEG image loaded successfully.";
    13 } else {
    14 qDebug() << "Failed to load JPEG image.";
    15 }

    ② 使用 QPixmap 加载图像

    QPixmap 也提供了 QPixmap::load() 函数用于加载图像文件,用法与 QImage 类似。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 bool QPixmap::load(const QString &fileName, const char *format = nullptr, Qt::ImageConversionFlags flags = Qt::AutoColor)

    fileName: 图像文件路径。
    format: 图像格式,可以为 nullptr
    flags: 图像转换标志,用于控制颜色转换等行为,通常使用默认值 Qt::AutoColor

    QPixmap::load() 函数返回 bool 值,表示加载是否成功。加载成功后,图像数据会存储在 QPixmap 对象中。

    示例:使用 QPixmap 加载图像

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPixmap pixmap;
    2 if (pixmap.load(":/images/background.png")) { // 从资源文件加载图像
    3 // 加载成功
    4 qDebug() << "Pixmap image loaded successfully.";
    5 } else {
    6 qDebug() << "Failed to load Pixmap image.";
    7 }

    ③ 从 QImage 转换为 QPixmap

    在某些情况下,你可能需要先使用 QImage 进行图像处理,然后再将处理后的图像显示在界面上。这时,可以使用 QPixmap::fromImage() 静态函数将 QImage 对象转换为 QPixmap 对象。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPixmap pixmap = QPixmap::fromImage(image); // image 是 QImage 对象

    ④ 从 QPixmap 转换为 QImage

    反之,如果需要对 QPixmap 对象进行像素级别的操作,可以使用 QPixmap::toImage() 函数将其转换为 QImage 对象。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QImage image = pixmap.toImage(); // pixmap 是 QPixmap 对象

    总结

    ⚝ 如果需要进行图像处理、像素操作,或者需要加载多种图像格式,优先选择 QImage。
    ⚝ 如果仅仅是加载和显示静态图像,并且对性能有较高要求,优先选择 QPixmap。
    ⚝ 可以在 QImage 和 QPixmap 之间方便地进行转换,根据具体需求灵活选择使用。

    Qt 支持多种常见的图像格式,包括 PNG, JPEG, BMP, GIF, TIFF, SVG 等。加载图像文件后,就可以在 GUI 界面上显示图像,或者对图像进行进一步的处理和操作。

    6.3.2 显示图像:QLabel, QGraphicsView

    介绍如何使用 QLabel 和 QGraphicsView 显示图像。

    Qt 提供了多种控件用于显示图像,最常用的包括 QLabel 和 QGraphicsView。

    ① 使用 QLabel 显示图像

    QLabel 是一个用于显示文本或图像的简单控件。它可以直接显示 QPixmap 或 QImage 对象。

    显示 QPixmap 图像:QLabel::setPixmap()

    可以使用 QLabel::setPixmap(const QPixmap &) 函数将 QPixmap 对象设置为 QLabel 的内容。QLabel 会自动调整自身大小以适应图像尺寸。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QLabel *imageLabel = new QLabel(this);
    2 QPixmap pixmap(":/images/qt_logo.png"); // 加载 QPixmap 图像
    3 imageLabel->setPixmap(pixmap); // 设置 QLabel 显示 QPixmap
    4 imageLabel->show();

    显示 QImage 图像:先转换为 QPixmap

    QLabel 本身不直接支持显示 QImage,需要先将 QImage 转换为 QPixmap,然后再使用 setPixmap() 显示。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QLabel *imageLabel = new QLabel(this);
    2 QImage image(":/images/qt_logo.png"); // 加载 QImage 图像
    3 QPixmap pixmap = QPixmap::fromImage(image); // 转换为 QPixmap
    4 imageLabel->setPixmap(pixmap); // 设置 QLabel 显示 QPixmap
    5 imageLabel->show();

    调整 QLabel 大小:QLabel::setScaledContents()

    默认情况下,QLabel 会自动调整大小以适应图像尺寸。如果需要 QLabel 的大小固定,并让图像缩放以适应 QLabel 的尺寸,可以使用 QLabel::setScaledContents(true) 函数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QLabel *imageLabel = new QLabel(this);
    2 QPixmap pixmap(":/images/qt_logo.png");
    3 imageLabel->setPixmap(pixmap);
    4 imageLabel->setFixedSize(200, 150); // 固定 QLabel 大小
    5 imageLabel->setScaledContents(true); // 图像缩放以适应 QLabel 尺寸
    6 imageLabel->show();

    ② 使用 QGraphicsView 和 QGraphicsScene 显示图像

    QGraphicsView 和 QGraphicsScene 是 Qt 的图形视图框架 (Graphics View Framework) 的核心组件,用于显示和交互大量的 2D 图形项。显示图像只是 QGraphicsView 的一个基本功能,它更强大,适用于复杂的 2D 场景。

    创建 QGraphicsScene 和 QGraphicsView

    首先需要创建一个 QGraphicsScene 对象,作为图形项的容器,然后创建一个 QGraphicsView 对象,用于显示场景的内容。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QGraphicsScene *scene = new QGraphicsScene(this);
    2 QGraphicsView *view = new QGraphicsView(scene, this);
    3 view->show();

    创建 QGraphicsPixmapItem 显示图像

    使用 QGraphicsPixmapItem 类创建一个图像项,并将 QPixmap 对象设置给它,然后将图像项添加到场景中。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPixmap pixmap(":/images/qt_logo.png");
    2 QGraphicsPixmapItem *pixmapItem = new QGraphicsPixmapItem(pixmap);
    3 scene->addItem(pixmapItem); // 将图像项添加到场景

    QGraphicsView 的优点

    ▮▮▮▮⚝ 缩放和旋转: QGraphicsView 支持场景的缩放和旋转操作,用户可以通过鼠标滚轮或手势进行交互。
    ▮▮▮▮⚝ 高性能: QGraphicsView 针对大量图形项的显示和交互进行了优化,性能更高。
    ▮▮▮▮⚝ 复杂场景: QGraphicsView 可以显示复杂的 2D 场景,包括多个图像、矢量图形、文本等,并支持图层管理、碰撞检测等高级功能。

    示例:使用 QGraphicsView 显示图像并支持缩放

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QGraphicsScene *scene = new QGraphicsScene(this);
    2 QGraphicsView *view = new QGraphicsView(scene, this);
    3
    4 QPixmap pixmap(":/images/world_map.png");
    5 QGraphicsPixmapItem *pixmapItem = new QGraphicsPixmapItem(pixmap);
    6 scene->addItem(pixmapItem);
    7
    8 view->setRenderHint(QPainter::Antialiasing); // 抗锯齿渲染
    9 view->setDragMode(QGraphicsView::ScrollHandDrag); // 拖拽模式
    10 view->setTransformationAnchor(QGraphicsView::AnchorViewCenter); // 缩放中心点
    11
    12 // 鼠标滚轮事件处理,实现缩放
    13 view->viewport()->installEventFilter(this); // 在 viewport 上安装事件过滤器
    14
    15 view->show();
    16
    17 // 事件过滤器,处理鼠标滚轮事件
    18 bool MyWidget::eventFilter(QObject *watched, QEvent *event)
    19 {
    20 if (watched == view->viewport() && event->type() == QEvent::Wheel) {
    21 QWheelEvent *wheelEvent = static_cast<QWheelEvent*>(event);
    22 double scaleFactor = 1.15;
    23 if (wheelEvent->delta() > 0) {
    24 view->scale(scaleFactor, scaleFactor); // 放大
    25 } else {
    26 view->scale(1.0 / scaleFactor, 1.0 / scaleFactor); // 缩小
    27 }
    28 return true; // 阻止事件继续传播
    29 }
    30 return QWidget::eventFilter(watched, event);
    31 }

    总结

    ⚝ 对于简单的图像显示,QLabel 足够使用,简单方便。
    ⚝ 对于需要缩放、旋转、显示复杂场景,或者需要高性能图形渲染的应用,QGraphicsView 是更强大的选择。

    在实际应用中,可以根据图像显示的复杂程度和交互需求选择合适的控件。如果只是静态显示少量图像,QLabel 是首选;如果需要显示大量图像,或者需要支持用户交互,QGraphicsView 则更适合。

    6.3.3 图像的基本操作:缩放、旋转、裁剪

    讲解图像的基本操作,如缩放、旋转和裁剪。

    QImage 和 QPixmap 都提供了一些函数用于进行图像的基本操作,例如缩放、旋转和裁剪。

    ① 图像缩放 (Scaling)

    QImage::scaled(): 返回一个缩放后的新 QImage 对象,原始图像保持不变。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QImage QImage::scaled(int width, int height, Qt::AspectRatioMode aspectRatioMode = Qt::IgnoreAspectRatio, Qt::TransformationMode transformMode = Qt::FastTransformation) const
    2 QImage QImage::scaled(const QSize &size, Qt::AspectRatioMode aspectRatioMode = Qt::IgnoreAspectRatio, Qt::TransformationMode transformMode = Qt::FastTransformation) const

    ▮▮▮▮⚝ width, heightsize: 目标图像的宽度和高度。
    ▮▮▮▮⚝ aspectRatioMode: 宽高比模式,控制缩放时是否保持宽高比,例如 Qt::KeepAspectRatio (保持宽高比,内容居中裁剪), Qt::IgnoreAspectRatio (忽略宽高比,可能变形), Qt::KeepAspectRatioByExpanding (保持宽高比,内容完整显示,画布可能扩大)。
    ▮▮▮▮⚝ transformMode: 变换模式,控制缩放算法,例如 Qt::FastTransformation (快速,可能图像质量稍差), Qt::SmoothTransformation (平滑,图像质量较好,但速度稍慢)。

    QPixmap::scaled(): 返回一个缩放后的新 QPixmap 对象,原始图像保持不变。参数与 QImage::scaled() 类似。

    示例:缩放 QImage 和 QPixmap

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QImage originalImage(":/images/original_image.png");
    2 QPixmap originalPixmap(":/images/original_image.png");
    3
    4 // 缩放到 200x150,保持宽高比,快速变换
    5 QImage scaledImage = originalImage.scaled(200, 150, Qt::KeepAspectRatio, Qt::FastTransformation);
    6 QPixmap scaledPixmap = originalPixmap.scaled(200, 150, Qt::KeepAspectRatio, Qt::FastTransformation);
    7
    8 // 缩放到 100x100,忽略宽高比,平滑变换
    9 QImage distortedImage = originalImage.scaled(100, 100, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
    10 QPixmap distortedPixmap = originalPixmap.scaled(100, 100, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);

    ② 图像旋转 (Rotation)

    QImage::transformed(): 返回一个旋转后的新 QImage 对象,原始图像保持不变。需要使用 QTransform 类来定义旋转变换。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QImage QImage::transformed(const QTransform &matrix, Qt::TransformationMode mode = Qt::FastTransformation) const

    ▮▮▮▮⚝ matrix: QTransform 变换矩阵,用于定义旋转、缩放、平移等变换。
    ▮▮▮▮⚝ mode: 变换模式,与缩放类似。

    要实现旋转,需要创建一个 QTransform 对象,并使用 QTransform::rotate() 函数设置旋转角度。

    QPixmap::transformed(): 返回一个旋转后的新 QPixmap 对象,用法与 QImage::transformed() 类似。

    示例:旋转 QImage 和 QPixmap

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QImage originalImage(":/images/original_image.png");
    2 QPixmap originalPixmap(":/images/original_image.png");
    3
    4 // 旋转 45 度,快速变换
    5 QTransform transform;
    6 transform.rotate(45);
    7 QImage rotatedImage = originalImage.transformed(transform, Qt::FastTransformation);
    8 QPixmap rotatedPixmap = originalPixmap.transformed(transform, Qt::FastTransformation);
    9
    10 // 旋转 -30 度,平滑变换
    11 QTransform transform2;
    12 transform2.rotate(-30);
    13 QImage rotatedImage2 = originalImage.transformed(transform2, Qt::SmoothTransformation);
    14 QPixmap rotatedPixmap2 = originalPixmap.transformed(transform2, Qt::SmoothTransformation);

    ③ 图像裁剪 (Cropping)

    QImage::copy(): 返回一个裁剪后的新 QImage 对象,原始图像保持不变。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QImage QImage::copy(const QRect &rect = QRect()) const

    ▮▮▮▮⚝ rect: 裁剪区域,QRect 对象。如果为空 (默认),则复制整个图像。

    QPixmap::copy(): 返回一个裁剪后的新 QPixmap 对象,用法与 QImage::copy() 类似。

    示例:裁剪 QImage 和 QPixmap

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QImage originalImage(":/images/original_image.png");
    2 QPixmap originalPixmap(":/images/original_image.png");
    3
    4 // 裁剪图像中心区域 100x100
    5 QRect cropRect(originalImage.width() / 2 - 50, originalImage.height() / 2 - 50, 100, 100);
    6 QImage croppedImage = originalImage.copy(cropRect);
    7 QPixmap croppedPixmap = originalPixmap.copy(cropRect);

    ④ 其他图像操作

    QImage 和 QPixmap 还提供了一些其他的图像操作函数,例如:

    镜像 (Mirroring): QImage::mirrored(), QPixmap::mirrored()
    颜色反转 (Invert Colors): QImage::invertPixels() (QPixmap 没有直接提供此功能,需要转换为 QImage 后操作)
    图像格式转换 (Format Conversion): QImage::convertToFormat(), QPixmap::convertFromImage()

    这些基本的图像操作可以满足大部分 GUI 应用的需求。如果需要更复杂的图像处理功能,例如滤镜、颜色调整、图像分析等,可以考虑使用 Qt 的图像处理模块 (Qt Image Processing Module,在 Qt 6 中已移除,可以考虑使用第三方库,如 OpenCV)。

    6.4 自定义控件 (Custom Widgets) 的绘制

    指导读者如何创建自定义控件,并在自定义控件中进行绘制操作。

    6.4.1 继承 QWidget 创建自定义控件

    讲解如何继承 QWidget 类创建自定义控件。

    自定义控件 (Custom Widgets) 是指开发者根据自身需求创建的、具有特定外观和行为的 GUI 控件。Qt 框架提供了强大的控件体系,但有时内置控件无法完全满足需求,这时就需要自定义控件。

    创建自定义控件的基本步骤:

    选择基类: 通常情况下,自定义控件会继承自 QWidget 类,因为 QWidget 是所有可视控件的基类,提供了最基本的功能,例如窗口系统集成、事件处理、绘制机制等。如果你的自定义控件需要包含子控件,或者需要更复杂的布局管理,也可以考虑继承自 QFrame, QScrollArea, QAbstractScrollArea 等更高级的基类。

    创建新的 C++ 类: 创建一个新的 C++ 类,继承自选择的基类 (通常是 QWidget)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // mywidget.h
    2 #ifndef MYWIDGET_H
    3 #define MYWIDGET_H
    4
    5 #include <QWidget>
    6
    7 class MyWidget : public QWidget
    8 {
    9 Q_OBJECT // 启用元对象系统特性
    10
    11 public:
    12 MyWidget(QWidget *parent = nullptr);
    13 ~MyWidget() override;
    14
    15 protected:
    16 void paintEvent(QPaintEvent *event) override; // 重写 paintEvent() 函数进行绘制
    17
    18 private:
    19 // ... 自定义控件的数据成员 ...
    20 };
    21
    22 #endif // MYWIDGET_H
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // mywidget.cpp
    2 #include "mywidget.h"
    3 #include <QPainter>
    4
    5 MyWidget::MyWidget(QWidget *parent)
    6 : QWidget(parent)
    7 {
    8 // 初始化自定义控件
    9 }
    10
    11 MyWidget::~MyWidget()
    12 {
    13 }
    14
    15 void MyWidget::paintEvent(QPaintEvent *event)
    16 {
    17 QPainter painter(this);
    18 painter.begin(this);
    19
    20 // 在 paintEvent() 函数中进行自定义绘制操作
    21
    22 painter.end();
    23 }

    添加 Q_OBJECT: 在自定义控件的类定义中,必须添加 Q_OBJECT,以启用 Qt 的元对象系统 (Meta-Object System) 特性,例如信号与槽、属性系统等。

    重写 paintEvent() 函数: paintEvent() 函数是自定义控件绘制的核心。当控件需要重绘时 (例如窗口初次显示、大小改变、内容更新等),Qt 会自动调用 paintEvent() 函数。你需要在 paintEvent() 函数中创建 QPainter 对象,并使用 QPainter 的 API 进行自定义绘制。

    实现自定义绘制逻辑: 在 paintEvent() 函数中,根据你的控件需求,使用 QPainter 绘制各种图形、文本、图像等,实现自定义控件的外观。你可以使用之前介绍的 QPainter 的各种绘图函数、绘图属性、坐标变换等技巧。

    添加自定义属性、信号、槽 (可选): 根据需要,你可以为自定义控件添加自定义属性 (使用 Qt 的属性系统)、信号和槽 (用于事件响应和通信)。

    在界面中使用自定义控件: 编译并生成包含自定义控件的程序或库。在 Qt Designer 或代码中,就可以像使用内置控件一样使用你的自定义控件了。

    示例:创建一个简单的圆形控件

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // circlewidget.h
    2 #ifndef CIRCLEWIDGET_H
    3 #define CIRCLEWIDGET.H
    4
    5 #include <QWidget>
    6 #include <QColor>
    7
    8 class CircleWidget : public QWidget
    9 {
    10 Q_OBJECT
    11
    12 public:
    13 CircleWidget(QWidget *parent = nullptr);
    14 ~CircleWidget() override;
    15
    16 QColor color() const { return m_color; }
    17 void setColor(const QColor &color);
    18
    19 protected:
    20 void paintEvent(QPaintEvent *event) override;
    21
    22 private:
    23 QColor m_color = Qt::red; // 圆形颜色属性
    24 };
    25
    26 #endif // CIRCLEWIDGET_H
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // circlewidget.cpp
    2 #include "circlewidget.h"
    3 #include <QPainter>
    4
    5 CircleWidget::CircleWidget(QWidget *parent)
    6 : QWidget(parent)
    7 {
    8 // 默认构造函数
    9 }
    10
    11 CircleWidget::~CircleWidget()
    12 {
    13 }
    14
    15 QColor CircleWidget::color() const
    16 {
    17 return m_color;
    18 }
    19
    20 void CircleWidget::setColor(const QColor &color)
    21 {
    22 m_color = color;
    23 update(); // 触发控件重绘
    24 }
    25
    26 void CircleWidget::paintEvent(QPaintEvent *event)
    27 {
    28 QPainter painter(this);
    29 painter.begin(this);
    30
    31 painter.setRenderHint(QPainter::Antialiasing); // 启用抗锯齿
    32
    33 QBrush brush(m_color);
    34 painter.setBrush(brush);
    35 painter.setPen(Qt::NoPen);
    36
    37 // 绘制圆形,大小为控件的可用区域
    38 painter.drawEllipse(rect());
    39
    40 painter.end();
    41 }

    paintEvent() 函数中,我们设置了抗锯齿渲染,并使用 m_color 属性作为填充颜色绘制了一个圆形,圆形的大小与控件的可用区域 (使用 rect() 函数获取) 相同。setColor() 函数用于设置圆形颜色,并在颜色改变后调用 update() 函数触发控件重绘。

    通过继承 QWidget 并重写 paintEvent() 函数,你可以创建各种各样的自定义控件,实现个性化的界面元素。

    6.4.2 在 paintEvent() 中进行自定义绘制

    介绍如何在自定义控件的 paintEvent() 函数中进行绘制操作。

    paintEvent() 函数是自定义控件绘制的核心入口点。当 Qt 框架需要控件重绘时,会自动调用 paintEvent() 函数。你需要在 paintEvent() 函数中完成所有自定义绘制操作。

    paintEvent() 函数的基本结构:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyWidget::paintEvent(QPaintEvent *event)
    2 {
    3 QPainter painter(this); // 创建 QPainter 对象,以当前控件作为绘图设备
    4 painter.begin(this); // 开始绘制 (可选,QPainter 构造函数已默认 begin)
    5
    6 // ... 自定义绘制代码 ...
    7
    8 painter.end(); // 结束绘制 (可选,QPainter 对象析构时会自动 end)
    9 }

    关键步骤和注意事项:

    创建 QPainter 对象: 在 paintEvent() 函数的开始,必须创建一个 QPainter 对象,并将当前控件 (this) 作为参数传递给 QPainter 的构造函数。这将 QPainter 与当前控件关联起来,使得绘制操作在当前控件上进行。

    begin()end() (可选): 虽然 QPainter::begin()QPainter::end() 函数是可选的 (因为 QPainter 构造函数默认会调用 begin(),析构函数默认会调用 end()),但为了代码的显式性和规范性,建议仍然在 paintEvent() 函数中显式地调用 begin()end() 函数。

    绘制代码: 在 begin()end() 之间,编写自定义的绘制代码。你可以使用 QPainter 提供的各种绘图函数 (drawLine(), drawRect(), drawEllipse(), drawText() 等)、绘图属性 (画笔 QPen, 画刷 QBrush, 字体 QFont)、坐标变换 (平移 translate(), 旋转 rotate(), 缩放 scale()) 等,实现控件的外观。

    优化绘制性能: paintEvent() 函数会被频繁调用,因此需要注意优化绘制性能,避免不必要的计算和绘制操作。常用的优化技巧包括:

    避免在 paintEvent() 中进行耗时操作: 例如文件 I/O, 网络请求, 大量数据计算等。这些操作应该在后台线程中完成,并在数据准备好后触发控件重绘。
    减少绘制操作: 尽量使用高效的绘制函数,避免不必要的复杂计算。
    使用缓存: 对于静态不变的部分,可以预先绘制到 QPixmap 或 QImage 中,然后在 paintEvent() 中直接绘制缓存的图像,避免重复绘制。
    裁剪和区域更新: 利用 paintEvent() 函数的 QPaintEvent 参数,可以获取需要更新的区域,只重绘需要更新的部分,避免整个控件重绘。

    触发重绘:update()repaint(): 当控件的数据或状态发生改变,需要更新外观时,需要手动触发控件的重绘。Qt 提供了 QWidget::update()QWidget::repaint() 两个函数用于触发重绘:

    update(): 请求异步重绘。Qt 会在事件循环的空闲时间合并多个 update() 请求,并在之后统一调用 paintEvent() 函数。update()推荐的重绘方式,效率更高,可以避免不必要的频繁重绘。
    repaint(): 请求同步重绘。会立即调用 paintEvent() 函数进行重绘,并阻塞当前线程直到重绘完成。repaint() 通常在需要立即更新界面的特殊情况下使用,例如动画、实时数据更新等,但频繁调用 repaint() 可能会降低程序性能。

    示例:在 paintEvent() 中绘制一个带边框和填充色的矩形

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyWidget::paintEvent(QPaintEvent *event)
    2 {
    3 QPainter painter(this);
    4 painter.begin(this);
    5
    6 painter.setRenderHint(QPainter::Antialiasing); // 启用抗锯齿
    7
    8 // 绘制填充色
    9 QBrush brush(Qt::lightGray);
    10 painter.setBrush(brush);
    11 painter.setPen(Qt::NoPen);
    12 painter.drawRect(rect()); // 填充整个控件区域
    13
    14 // 绘制边框
    15 QPen pen(Qt::darkGray);
    16 pen.setWidth(2);
    17 painter.setPen(pen);
    18 painter.setBrush(Qt::NoBrush); // 无填充
    19 painter.drawRect(rect().adjusted(5, 5, -5, -5)); // 绘制内缩 5 像素的边框
    20
    21 // 绘制文本
    22 QFont font;
    23 font.setPointSize(20);
    24 painter.setFont(font);
    25 painter.setPen(Qt::black);
    26 painter.drawText(rect(), Qt::AlignCenter, "Custom Widget"); // 居中绘制文本
    27
    28 painter.end();
    29 }

    paintEvent() 函数中,我们首先填充了控件的背景色,然后绘制了一个内缩的边框,最后居中绘制了文本 "Custom Widget"。通过组合不同的绘制操作,可以在 paintEvent() 函数中创建出各种自定义的界面元素。

    6.4.3 响应用户交互的自定义控件

    讲解如何使自定义控件响应用户交互,例如鼠标点击事件。

    自定义控件不仅需要有自定义的外观,还需要能够响应用户的交互操作,例如鼠标点击、键盘输入等。要使自定义控件能够响应用户交互,需要重写相应的事件处理函数

    常用的事件处理函数:

    鼠标事件:
    ▮▮▮▮⚝ mousePressEvent(QMouseEvent *event): 鼠标按键按下事件。
    ▮▮▮▮⚝ mouseReleaseEvent(QMouseEvent *event): 鼠标按键释放事件。
    ▮▮▮▮⚝ mouseDoubleClickEvent(QMouseEvent *event): 鼠标双击事件。
    ▮▮▮▮⚝ mouseMoveEvent(QMouseEvent *event): 鼠标移动事件。
    ▮▮▮▮⚝ wheelEvent(QWheelEvent *event): 鼠标滚轮事件。
    键盘事件:
    ▮▮▮▮⚝ keyPressEvent(QKeyEvent *event): 键盘按键按下事件。
    ▮▮▮▮⚝ keyReleaseEvent(QKeyEvent *event): 键盘按键释放事件。
    焦点事件:
    ▮▮▮▮⚝ focusInEvent(QFocusEvent *event): 控件获得焦点事件。
    ▮▮▮▮⚝ focusOutEvent(QFocusEvent *event): 控件失去焦点事件。
    其他事件: 还有窗口事件、拖放事件、定时器事件等。

    事件处理函数的基本结构:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyWidget::mousePressEvent(QMouseEvent *event)
    2 {
    3 // 处理鼠标按键按下事件
    4
    5 // 调用父类的事件处理函数,确保默认行为 (可选)
    6 QWidget::mousePressEvent(event);
    7 }

    关键步骤和注意事项:

    重写事件处理函数: 在自定义控件的类定义中,声明需要处理的事件处理函数,并使用 override 关键字 (如果基类函数是虚函数)。

    事件参数: 每个事件处理函数都接受一个事件对象指针作为参数,例如 QMouseEvent *event, QKeyEvent *event 等。事件对象包含了事件的详细信息,例如鼠标事件的鼠标位置、按键类型,键盘事件的按键代码、修饰符等。

    处理事件逻辑: 在事件处理函数中,编写自定义的事件处理逻辑。例如,在 mousePressEvent() 函数中,可以获取鼠标点击的位置,判断是否点击了控件的某个区域,并根据点击位置执行相应的操作。

    调用父类事件处理函数 (可选): 在自定义事件处理函数中,可以选择是否调用父类的同名事件处理函数。如果调用父类函数,可以确保控件的默认事件处理行为仍然有效。例如,对于鼠标事件,QWidget 的默认行为是处理控件的焦点和鼠标捕获等。如果不调用父类函数,则会完全自定义事件处理行为,可能会覆盖默认行为。

    触发控件重绘:update(): 在事件处理函数中,如果控件的状态或外观需要改变,通常需要调用 update() 函数触发控件重绘,以显示最新的状态。

    示例:创建一个可以响应鼠标点击的按钮控件

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // clickablebutton.h
    2 #ifndef CLICKABLEBUTTON_H
    3 #define CLICKABLEBUTTON_H
    4
    5 #include <QWidget>
    6 #include <QString>
    7 #include <QMouseEvent>
    8 #include <QPainter>
    9
    10 class ClickableButton : public QWidget
    11 {
    12 Q_OBJECT
    13
    14 public:
    15 ClickableButton(QWidget *parent = nullptr);
    16 ~ClickableButton() override;
    17
    18 QString text() const { return m_text; }
    19 void setText(const QString &text);
    20
    21 signals:
    22 void clicked(); // 点击信号
    23
    24 protected:
    25 void paintEvent(QPaintEvent *event) override;
    26 void mousePressEvent(QMouseEvent *event) override;
    27 void mouseReleaseEvent(QMouseEvent *event) override;
    28
    29 private:
    30 QString m_text = "Click Me";
    31 bool m_pressed = false; // 按下状态
    32 };
    33
    34 #endif // CLICKABLEBUTTON_H
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // clickablebutton.cpp
    2 #include "clickablebutton.h"
    3 #include <QPainter>
    4
    5 ClickableButton::ClickableButton(QWidget *parent)
    6 : QWidget(parent)
    7 {
    8 setMouseTracking(true); // 启用鼠标跟踪,即使没有按下鼠标也能接收 mouseMoveEvent
    9 }
    10
    11 ClickableButton::~ClickableButton()
    12 {
    13 }
    14
    15 QString ClickableButton::text() const
    16 {
    17 return m_text;
    18 }
    19
    20 void ClickableButton::setText(const QString &text)
    21 {
    22 m_text = text;
    23 update();
    24 }
    25
    26 void ClickableButton::paintEvent(QPaintEvent *event)
    27 {
    28 QPainter painter(this);
    29 painter.begin(this);
    30 painter.setRenderHint(QPainter::Antialiasing);
    31
    32 QColor bgColor = m_pressed ? Qt::lightGray : Qt::white; // 按下状态背景色变浅
    33 QBrush brush(bgColor);
    34 painter.setBrush(brush);
    35 QPen pen(Qt::black);
    36 painter.setPen(pen);
    37 painter.drawRect(rect().adjusted(1, 1, -1, -1)); // 绘制按钮边框
    38
    39 QFont font;
    40 font.setPointSize(16);
    41 painter.setFont(font);
    42 painter.drawText(rect(), Qt::AlignCenter, m_text);
    43
    44 painter.end();
    45 }
    46
    47 void ClickableButton::mousePressEvent(QMouseEvent *event)
    48 {
    49 if (event->button() == Qt::LeftButton) {
    50 m_pressed = true; // 设置按下状态
    51 update(); // 触发重绘,改变外观
    52 }
    53 QWidget::mousePressEvent(event); // 调用父类处理函数
    54 }
    55
    56 void ClickableButton::mouseReleaseEvent(QMouseEvent *event)
    57 {
    58 if (m_pressed && event->button() == Qt::LeftButton) {
    59 m_pressed = false; // 恢复非按下状态
    60 update(); // 触发重绘
    61 emit clicked(); // 发射 clicked 信号
    62 }
    63 QWidget::mouseReleaseEvent(event);
    64 }

    ClickableButton 控件中,我们重写了 mousePressEvent()mouseReleaseEvent() 函数,用于处理鼠标按下和释放事件。当鼠标左键按下时,设置 m_pressed 状态为 true 并触发重绘,使按钮外观变为按下状态;当鼠标左键释放时,恢复 m_pressed 状态为 false 并触发重绘,同时发射 clicked() 信号,通知外部控件按钮被点击了。在 paintEvent() 函数中,我们根据 m_pressed 状态改变按钮的背景色,实现按下效果。

    通过重写事件处理函数,并结合信号与槽机制,可以使自定义控件具备丰富的交互能力,响应用户的各种操作。自定义控件的交互逻辑可以根据具体需求进行灵活设计,实现各种定制化的用户交互体验。

    7. 模型/视图架构:数据与界面的分离

    概要

    本章将深入探讨 Qt 框架中的模型/视图架构 (Model/View Architecture)。模型/视图架构是一种强大的设计模式,它将数据的存储和管理(模型,Model)与数据的可视化呈现(视图,View)以及用户交互控制(委托,Delegate)有效地分离。这种分离极大地提高了 GUI 应用程序的模块化程度、可维护性和灵活性。通过本章的学习,读者将能够理解模型/视图架构的核心概念,掌握如何使用 Qt 提供的模型和视图类,以及如何自定义模型和委托,从而构建更加健壮和可扩展的 GUI 应用程序。

    7.1 模型/视图架构概述

    7.1.1 模型 (Model)、视图 (View)、委托 (Delegate) 的角色与关系

    模型/视图架构的核心思想是将数据、数据的呈现和用户的交互操作分离开来,这三个核心组件分别是:

    模型 (Model)
    ▮▮▮▮模型是整个架构的数据来源和管理者。它负责存储应用程序的数据,并提供访问和操作数据的方法。模型独立于数据的显示方式,这意味着无论数据最终以列表、表格还是树形结构展示,模型都只需要关注数据的逻辑结构和操作。在 Qt 中,模型通常继承自 QAbstractItemModel 类或其子类,例如 QStringListModelQStandardItemModel 等。模型需要实现特定的接口,以便视图和委托能够访问和操作数据。

    视图 (View)
    ▮▮▮▮视图负责数据的可视化呈现。它从模型中获取数据,并将其以某种用户友好的方式显示出来,例如列表、表格或树形结构。视图并不存储数据,它只是数据的观察者。视图响应用户的操作,例如选择、点击等,并将这些操作转化为对模型的请求。Qt 提供了多种预定义的视图类,例如 QListView(列表视图)、QTableView(表格视图)和 QTreeView(树形视图),可以满足不同场景下的数据展示需求。

    委托 (Delegate)
    ▮▮▮▮委托负责控制视图中数据的渲染 (rendering)编辑 (editing) 方式。当视图需要显示模型中的数据项时,它会请求委托来绘制该数据项。同样,当用户想要编辑数据项时,视图也会使用委托来提供编辑器 (editor) 并处理用户的输入。委托使得我们可以高度定制数据项的显示外观和编辑行为。Qt 提供了默认的委托实现 QStyledItemDelegate,同时也允许开发者创建自定义的委托以满足特定的需求。

    这三个组件之间的关系可以用下图来表示:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 graph LR
    2 Model --> View: 提供数据
    3 View --> Model: 请求数据和操作
    4 Model --> Delegate: 提供数据用于渲染
    5 Delegate --> View: 渲染数据项
    6 View --> Delegate: 请求编辑器
    7 Delegate --> Model: 更新数据
    8 User -- 操作 --> View

    从图中可以看出,模型是核心,视图和委托都与模型交互。视图负责数据的呈现和用户交互,而委托则负责更细粒度的数据项的渲染和编辑控制。这种分离使得各个组件的职责更加明确,提高了代码的可维护性和可复用性。

    7.1.2 模型/视图架构的优势:数据与界面分离、代码复用

    采用模型/视图架构带来了诸多优势,主要体现在以下几个方面:

    数据与界面分离 (Separation of Data and Presentation)
    ▮▮▮▮这是模型/视图架构最核心的优势。将数据管理(模型)和数据呈现(视图)分离开来,使得应用程序的逻辑结构更加清晰。模型专注于数据处理和业务逻辑,而视图则专注于用户界面的展示。这种分离使得修改数据逻辑不会影响界面代码,反之亦然,降低了维护成本,提高了开发效率。

    代码复用 (Code Reusability)
    ▮▮▮▮由于模型和视图是独立的组件,同一个模型可以被多个不同的视图复用。例如,同一个数据模型可以同时用列表视图和表格视图来展示,而无需修改模型代码。同样,同一个视图也可以用于展示不同的模型数据,只要模型遵循了模型/视图架构的接口规范。这种代码复用性极大地减少了重复开发工作,提高了代码的利用率。

    灵活性和可扩展性 (Flexibility and Extensibility)
    ▮▮▮▮模型/视图架构提供了高度的灵活性和可扩展性。开发者可以根据需求选择合适的预定义模型和视图,也可以自定义模型、视图和委托,以满足特定的应用场景。例如,可以自定义模型来适配特定的数据源(如网络数据、文件数据等),可以自定义视图来呈现特殊的数据展示效果,可以自定义委托来控制数据项的特殊渲染和编辑行为。这种灵活性使得模型/视图架构能够适应各种复杂的 GUI 应用程序需求。

    易于测试 (Testability)
    ▮▮▮▮由于模型层独立于视图层,模型可以单独进行单元测试,验证数据逻辑的正确性。视图层的测试也相对独立,可以专注于界面呈现和用户交互的测试。这种分离使得测试更加容易进行,提高了应用程序的质量。

    7.2 模型 (Model) 的实现

    模型 (Model) 是模型/视图架构的核心组件,它负责管理和提供数据。在 Qt 中,模型需要继承自抽象基类 QAbstractItemModel 或其子类。Qt 框架已经提供了一些常用的模型类,可以直接使用,也可以根据需要创建自定义模型。

    7.2.1 Qt 提供的模型类:QStringListModel, QStandardItemModel, QFileSystemModel, QSqlQueryModel

    Qt 提供了一些预定义的模型类,可以满足常见的应用场景:

    QStringListModel
    ▮▮▮▮QStringListModel 是最简单的模型之一,它用于管理字符串列表数据。它将一个 QStringList 对象作为数据源,并将其中的每个字符串作为一个数据项。QStringListModel 适用于展示简单的列表数据,例如选项列表、日志列表等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QStringList stringList;
    2 stringList << "Item 1" << "Item 2" << "Item 3";
    3 QStringListModel *model = new QStringListModel(stringList);
    4
    5 QListView *listView = new QListView();
    6 listView->setModel(model); // 将模型设置给视图
    7 listView->show();

    ▮▮▮▮在上述代码中,我们创建了一个 QStringListModel 对象,并将一个字符串列表设置为其数据源。然后,我们创建了一个 QListView 对象,并将模型设置给视图,这样列表视图就会显示字符串列表中的数据。

    QStandardItemModel
    ▮▮▮▮QStandardItemModel 是一个通用的、基于项 (item) 的模型。它以树形结构组织数据,每个数据项都是一个 QStandardItem 对象。QStandardItem 可以存储各种类型的数据,例如文本、图标、复选框状态等,并且可以设置数据项的属性,例如是否可编辑、是否可选中等。QStandardItemModel 适用于展示复杂的数据结构,例如树形结构、表格数据等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QStandardItemModel *model = new QStandardItemModel();
    2 QStandardItem *parentItem = model->invisibleRootItem(); // 获取根项
    3
    4 QStandardItem *item1 = new QStandardItem("Group 1");
    5 QStandardItem *child1_1 = new QStandardItem("Item 1.1");
    6 QStandardItem *child1_2 = new QStandardItem("Item 1.2");
    7 item1->appendRow(child1_1);
    8 item1->appendRow(child1_2);
    9 parentItem->appendRow(item1);
    10
    11 QStandardItem *item2 = new QStandardItem("Group 2");
    12 QStandardItem *child2_1 = new QStandardItem("Item 2.1");
    13 item2->appendRow(child2_1);
    14 parentItem->appendRow(item2);
    15
    16 QTreeView *treeView = new QTreeView();
    17 treeView->setModel(model);
    18 treeView->expandAll(); // 展开所有节点
    19 treeView->show();

    ▮▮▮▮上述代码创建了一个 QStandardItemModel 模型,并构建了一个简单的树形结构数据。然后,将模型设置给 QTreeView,树形视图就会显示该树形数据。

    QFileSystemModel
    ▮▮▮▮QFileSystemModel 是一个专门用于展示文件系统目录和文件的模型。它从文件系统中读取文件和目录信息,并将其以树形结构展示。QFileSystemModel 可以方便地用于创建文件浏览器等应用程序。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QFileSystemModel *model = new QFileSystemModel();
    2 model->setRootPath(QDir::homePath()); // 设置根目录为用户主目录
    3
    4 QTreeView *treeView = new QTreeView();
    5 treeView->setModel(model);
    6 treeView->setRootIndex(model->index(QDir::homePath())); // 设置视图的根索引
    7 treeView->show();

    ▮▮▮▮这段代码创建了一个 QFileSystemModel 模型,并将根路径设置为用户主目录。然后,将模型设置给 QTreeView,树形视图就会显示用户主目录下的文件和目录结构。

    QSqlQueryModel
    ▮▮▮▮QSqlQueryModel 用于展示数据库查询结果。它执行 SQL 查询语句,并将查询结果作为模型数据。QSqlQueryModel 可以方便地将数据库数据展示在 GUI 界面中。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    2 db.setDatabaseName(":memory:"); // 使用内存数据库
    3 if (!db.open()) {
    4 qDebug() << "Error opening database:" << db.lastError().text();
    5 return;
    6 }
    7
    8 QSqlQuery query(db);
    9 query.exec("CREATE TABLE persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)");
    10 query.exec("INSERT INTO persons (name, age) VALUES ('Alice', 30)");
    11 query.exec("INSERT INTO persons (name, age) VALUES ('Bob', 25)");
    12
    13 QSqlQueryModel *model = new QSqlQueryModel();
    14 model->setQuery("SELECT name, age FROM persons"); // 执行 SQL 查询
    15
    16 QTableView *tableView = new QTableView();
    17 tableView->setModel(model);
    18 tableView->show();
    19
    20 db.close();

    ▮▮▮▮上述代码首先创建了一个 SQLite 内存数据库,并创建了一个 persons 表格,插入了两条数据。然后,创建了一个 QSqlQueryModel 模型,并执行 SQL 查询语句 SELECT name, age FROM persons。最后,将模型设置给 QTableView,表格视图就会显示查询结果。

    7.2.2 创建自定义模型:继承 QAbstractListModel, QAbstractTableModel, QAbstractItemModel

    当 Qt 提供的预定义模型类不能满足需求时,就需要创建自定义模型。Qt 提供了三个抽象模型基类,用于创建不同类型的自定义模型:

    QAbstractListModel
    ▮▮▮▮QAbstractListModel 是用于创建列表模型 (list model) 的基类。列表模型将数据组织成一维列表,适用于展示线性数据,例如简单的项目列表。自定义列表模型需要继承 QAbstractListModel 并实现以下纯虚函数 (pure virtual functions)
    ▮▮▮▮⚝ int rowCount(const QModelIndex &parent = QModelIndex()) const:返回模型的行数。对于列表模型,列数始终为 1。parent 参数在列表模型中通常被忽略,因为列表模型是一维的。
    ▮▮▮▮⚝ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const:返回指定索引的数据。index 参数指定数据项的索引,role 参数指定数据的角色(例如,显示角色、编辑角色等)。
    ▮▮▮▮⚝ QHash<int, QByteArray> roleNames() const override (可选):返回角色名称映射,用于在 QML 中使用角色名而不是角色数字。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QAbstractListModel>
    2 #include <QVariant>
    3 #include <QStringList>
    4
    5 class MyListModel : public QAbstractListModel {
    6 Q_OBJECT
    7 public:
    8 MyListModel(const QStringList &strings, QObject *parent = nullptr)
    9 : QAbstractListModel(parent), stringList(strings) {}
    10
    11 int rowCount(const QModelIndex &parent = QModelIndex()) const override {
    12 Q_UNUSED(parent);
    13 return stringList.size();
    14 }
    15
    16 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
    17 if (!index.isValid())
    18 return QVariant();
    19 if (index.row() >= 0 && index.row() < stringList.size() && role == Qt::DisplayRole)
    20 return stringList[index.row()];
    21 return QVariant();
    22 }
    23
    24 private:
    25 QStringList stringList;
    26 };

    ▮▮▮▮上述代码示例创建了一个名为 MyListModel 的自定义列表模型,它继承自 QAbstractListModel,并实现了 rowCount()data() 函数。该模型使用一个 QStringList 作为数据源。

    QAbstractTableModel
    ▮▮▮▮QAbstractTableModel 是用于创建表格模型 (table model) 的基类。表格模型将数据组织成二维表格,适用于展示表格数据,例如电子表格、数据库表格等。自定义表格模型需要继承 QAbstractTableModel 并实现以下纯虚函数:
    ▮▮▮▮⚝ int rowCount(const QModelIndex &parent = QModelIndex()) const:返回模型的行数。parent 参数在表格模型中通常被忽略。
    ▮▮▮▮⚝ int columnCount(const QModelIndex &parent = QModelIndex()) const:返回模型的列数。parent 参数在表格模型中通常被忽略。
    ▮▮▮▮⚝ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const:返回指定索引的数据。index 参数指定数据项的行和列索引,role 参数指定数据的角色。
    ▮▮▮▮⚝ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override (可选):返回表头 (header) 数据。section 参数指定表头的索引(行或列),orientation 参数指定表头的方向(水平或垂直),role 参数指定数据的角色。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QAbstractTableModel>
    2 #include <QVariant>
    3 #include <QList>
    4
    5 class MyTableModel : public QAbstractTableModel {
    6 Q_OBJECT
    7 public:
    8 MyTableModel(const QList<QList<QVariant>> &data, QObject *parent = nullptr)
    9 : QAbstractTableModel(parent), tableData(data) {}
    10
    11 int rowCount(const QModelIndex &parent = QModelIndex()) const override {
    12 Q_UNUSED(parent);
    13 return tableData.size();
    14 }
    15
    16 int columnCount(const QModelIndex &parent = QModelIndex()) const override {
    17 Q_UNUSED(parent);
    18 return tableData.isEmpty() ? 0 : tableData[0].size(); // 假设每行数据列数相同
    19 }
    20
    21 QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
    22 if (!index.isValid())
    23 return QVariant();
    24 if (index.row() >= 0 && index.row() < tableData.size() &&
    25 index.column() >= 0 && index.column() < tableData[0].size() &&
    26 role == Qt::DisplayRole)
    27 return tableData[index.row()][index.column()];
    28 return QVariant();
    29 }
    30
    31 private:
    32 QList<QList<QVariant>> tableData;
    33 };

    ▮▮▮▮上述代码示例创建了一个名为 MyTableModel 的自定义表格模型,它继承自 QAbstractTableModel,并实现了 rowCount()columnCount()data() 函数。该模型使用一个 QList<QList<QVariant>> 作为数据源。

    QAbstractItemModel
    ▮▮▮▮QAbstractItemModel 是最通用的抽象模型基类,它是 QAbstractListModelQAbstractTableModel 的基类。QAbstractItemModel 可以用于创建各种类型的模型,包括列表模型、表格模型、树形模型等。自定义 QAbstractItemModel 模型需要实现更多的纯虚函数,以支持更复杂的数据结构和操作。常用的纯虚函数包括:
    ▮▮▮▮⚝ int rowCount(const QModelIndex &parent = QModelIndex()) const
    ▮▮▮▮⚝ int columnCount(const QModelIndex &parent = QModelIndex()) const
    ▮▮▮▮⚝ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const
    ▮▮▮▮⚝ QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const:根据行、列和父索引创建模型索引。
    ▮▮▮▮⚝ QModelIndex parent(const QModelIndex &child) const:返回给定索引的父索引。
    ▮▮▮▮⚝ Qt::ItemFlags flags(const QModelIndex &index) const override (可选):返回数据项的标志,例如是否可编辑、是否可选中等。
    ▮▮▮▮⚝ QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override (可选)
    ▮▮▮▮⚝ bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override (可选):设置模型中指定索引的数据。实现此函数可以使模型支持数据编辑。

    ▮▮▮▮选择继承哪个抽象模型基类取决于要创建的模型类型。如果只需要展示简单的列表数据,可以选择 QAbstractListModel。如果需要展示表格数据,可以选择 QAbstractTableModel。如果需要创建更复杂的模型,例如树形模型或具有特殊数据结构的模型,则需要继承 QAbstractItemModel

    7.2.3 模型的接口:data(), rowCount(), columnCount(), headerData()

    模型需要实现一些特定的接口函数,以便视图和委托能够访问和操作模型数据。最核心的接口函数包括:

    data(const QModelIndex &index, int role = Qt::DisplayRole) const
    ▮▮▮▮data() 函数是模型最核心的接口函数。它负责返回模型中指定索引的数据。index 参数是一个 QModelIndex 对象,它唯一标识了模型中的一个数据项。role 参数是一个整数,用于指定数据的角色 (role)。角色定义了数据的不同用途和属性,例如:
    ▮▮▮▮⚝ Qt::DisplayRole:用于在视图中显示的数据。这是最常用的角色。
    ▮▮▮▮⚝ Qt::EditRole:用于编辑的数据。通常与 Qt::DisplayRole 相同,但在某些情况下可能需要返回不同的数据格式。
    ▮▮▮▮⚝ Qt::DecorationRole:用于在视图中显示的装饰 (decoration),例如图标。
    ▮▮▮▮⚝ Qt::ToolTipRole:用于显示工具提示 (tooltip) 的文本。
    ▮▮▮▮⚝ Qt::CheckStateRole:用于复选框的状态。
    ▮▮▮▮⚝ ... 等等。Qt 提供了丰富的角色定义,可以满足各种数据展示和交互需求。

    ▮▮▮▮在 data() 函数中,需要根据 indexrole 参数,从模型的数据源中获取相应的数据,并将其封装成 QVariant 对象返回。如果索引无效或角色不支持,则应返回一个无效的 QVariant 对象 QVariant()

    rowCount(const QModelIndex &parent = QModelIndex()) const
    ▮▮▮▮rowCount() 函数返回模型中的行数。parent 参数用于处理层级模型 (hierarchical model),例如树形模型。对于列表模型和表格模型,parent 参数通常被忽略,或者只在根节点 (QModelIndex()) 时使用。在 rowCount() 函数中,需要根据模型的数据源,计算并返回行数。

    columnCount(const QModelIndex &parent = QModelIndex()) const
    ▮▮▮▮columnCount() 函数返回模型中的列数。与 rowCount() 类似,parent 参数也用于处理层级模型。对于列表模型,列数通常为 1。对于表格模型,列数取决于表格的列数。在 columnCount() 函数中,需要根据模型的数据源,计算并返回列数。

    headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const
    ▮▮▮▮headerData() 函数用于返回表头 (header) 数据。section 参数指定表头的索引(行或列),orientation 参数指定表头的方向(Qt::Horizontal 水平表头,Qt::Vertical 垂直表头),role 参数指定数据的角色。表头数据通常用于显示列名或行号。在 headerData() 函数中,需要根据 sectionorientationrole 参数,返回相应的表头数据。如果没有表头数据,则应返回无效的 QVariant 对象 QVariant()

    除了上述核心接口函数外,模型还可以实现其他可选的接口函数,例如:
    flags():返回数据项的标志,用于控制数据项的行为(例如,是否可编辑、是否可选中等)。
    setData():设置模型中指定索引的数据,实现数据编辑功能。
    insertRows(), removeRows(), insertColumns(), removeColumns():插入和删除行、列,实现动态数据更新。
    sort():对模型数据进行排序。

    实现这些可选接口函数可以增强模型的功能,使其支持更复杂的数据操作和交互。

    7.3 视图 (View) 的使用

    视图 (View) 负责数据的可视化呈现,它从模型中获取数据,并将其以用户友好的方式显示出来。Qt 提供了多种预定义的视图类,可以满足不同场景下的数据展示需求。

    7.3.1 Qt 提供的视图类:QListView, QTableView, QTreeView

    Qt 提供了三种主要的视图类:

    QListView
    ▮▮▮▮QListView 用于展示列表数据 (list data)。它将模型中的数据项以垂直列表的形式排列显示。QListView 适用于展示一维数据,例如文件列表、选项列表等。QListView 可以与 QAbstractListModelQAbstractItemModel 模型一起使用。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QStringListModel *model = new QStringListModel({"Item 1", "Item 2", "Item 3"});
    2 QListView *listView = new QListView();
    3 listView->setModel(model);
    4 listView->show();

    ▮▮▮▮上述代码创建了一个 QListView,并将其模型设置为 QStringListModel,列表视图将显示字符串列表中的数据。

    QTableView
    ▮▮▮▮QTableView 用于展示表格数据 (table data)。它将模型中的数据项以二维表格的形式排列显示。QTableView 适用于展示二维数据,例如电子表格、数据库表格等。QTableView 可以与 QAbstractTableModelQAbstractItemModel 模型一起使用。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QStandardItemModel *model = new QStandardItemModel(3, 2); // 3行 2列
    2 model->setHeaderData(0, Qt::Horizontal, "Name");
    3 model->setHeaderData(1, Qt::Horizontal, "Age");
    4 model->setItem(0, 0, new QStandardItem("Alice"));
    5 model->setItem(0, 1, new QStandardItem("30"));
    6 model->setItem(1, 0, new QStandardItem("Bob"));
    7 model->setItem(1, 1, new QStandardItem("25"));
    8 model->setItem(2, 0, new QStandardItem("Charlie"));
    9 model->setItem(2, 1, new QStandardItem("35"));
    10
    11 QTableView *tableView = new QTableView();
    12 tableView->setModel(model);
    13 tableView->show();

    ▮▮▮▮这段代码创建了一个 QTableView,并将其模型设置为 QStandardItemModel,表格视图将显示表格数据。

    QTreeView
    ▮▮▮▮QTreeView 用于展示树形数据 (tree data)。它将模型中的数据项以树形结构排列显示,支持节点的展开和折叠。QTreeView 适用于展示层级数据,例如文件系统目录结构、组织结构等。QTreeView 通常与 QAbstractItemModel 模型一起使用,特别是当模型实现了层级结构时。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QFileSystemModel *model = new QFileSystemModel();
    2 model->setRootPath(QDir::homePath());
    3 QTreeView *treeView = new QTreeView();
    4 treeView->setModel(model);
    5 treeView->setRootIndex(model->index(QDir::homePath()));
    6 treeView->show();

    ▮▮▮▮上述代码创建了一个 QTreeView,并将其模型设置为 QFileSystemModel,树形视图将显示文件系统目录结构。

    选择使用哪种视图取决于要展示的数据类型和用户界面的需求。QListView 适用于简单的列表展示,QTableView 适用于表格数据展示,QTreeView 适用于树形结构数据展示。

    7.3.2 视图与模型的连接:setModel()

    视图与模型的连接是通过 setModel() 函数实现的。每个视图类(QListView, QTableView, QTreeView)都提供了 setModel(QAbstractItemModel *model) 函数,用于将视图与一个模型对象关联起来。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 MyListModel *model = new MyListModel({"Item A", "Item B", "Item C"});
    2 QListView *listView = new QListView();
    3 listView->setModel(model); // 将模型设置给视图
    4
    5 MyTableModel *tableModel = new MyTableModel({
    6 { "Name", "Age" },
    7 { "Alice", 30 },
    8 { "Bob", 25 }
    9 });
    10 QTableView *tableView = new QTableView();
    11 tableView->setModel(tableModel); // 将模型设置给视图
    12
    13 QFileSystemModel *fileSystemModel = new QFileSystemModel();
    14 fileSystemModel->setRootPath(QDir::homePath());
    15 QTreeView *treeView = new QTreeView();
    16 treeView->setModel(fileSystemModel); // 将模型设置给视图

    ▮▮▮▮在上述代码示例中,我们分别创建了 QListViewQTableViewQTreeView,并使用 setModel() 函数将它们与相应的模型对象连接起来。一旦视图与模型连接成功,视图就会自动从模型中获取数据并显示出来。

    7.3.3 视图的配置:显示属性、选择模式、排序、过滤

    视图提供了丰富的配置选项,可以定制数据的显示方式和用户交互行为。常见的配置包括:

    显示属性 (Display Properties)
    ▮▮▮▮视图允许配置各种显示属性,例如:
    ▮▮▮▮⚝ 字体 (Font):设置视图中数据项的字体。
    ▮▮▮▮⚝ 颜色 (Color):设置视图的前景色和背景色。
    ▮▮▮▮⚝ 行高 (Row Height)列宽 (Column Width):设置表格视图和列表视图的行高和列宽。
    ▮▮▮▮⚝ 网格线 (Grid Lines):控制表格视图是否显示网格线。
    ▮▮▮▮⚝ 表头 (Headers):控制表格视图和树形视图是否显示表头,并可以自定义表头的内容和样式。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTableView *tableView = new QTableView();
    2 QFont font("Arial", 12);
    3 tableView->setFont(font); // 设置字体
    4 tableView->setStyleSheet("background-color: lightblue;"); // 设置背景色
    5 tableView->setShowGrid(false); // 隐藏网格线
    6 tableView->horizontalHeader()->setVisible(false); // 隐藏水平表头

    ▮▮▮▮上述代码示例配置了 QTableView 的字体、背景色、网格线和水平表头显示属性。

    选择模式 (Selection Mode)
    ▮▮▮▮视图支持多种选择模式 (selection mode),用于控制用户的选择行为。常见的选择模式包括:
    ▮▮▮▮⚝ NoSelection:禁用选择。
    ▮▮▮▮⚝ SingleSelection:单选模式,用户一次只能选择一个数据项。
    ▮▮▮▮⚝ MultiSelection:多选模式,用户可以选择多个数据项。
    ▮▮▮▮⚝ ExtendedSelection:扩展选择模式,用户可以使用 Shift 键和 Ctrl 键进行连续和不连续的多选。
    ▮▮▮▮⚝ ContiguousSelection:连续选择模式,用户只能选择连续的数据项。
    ▮▮▮▮⚝ SingleRowSelection, MultiRowSelection, ExtendedRowSelection, ContiguousRowSelection:行选择模式,用户选择的是整行。
    ▮▮▮▮⚝ SingleColumnSelection, MultiColumnSelection, ExtendedColumnSelection, ContiguousColumnSelection:列选择模式,用户选择的是整列。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QListView *listView = new QListView();
    2 listView->setSelectionMode(QAbstractItemView::MultiSelection); // 设置为多选模式

    ▮▮▮▮这段代码将 QListView 的选择模式设置为多选模式,用户可以在列表中选择多个项目。

    排序 (Sorting)
    ▮▮▮▮视图支持对数据进行排序 (sorting)。对于表格视图和树形视图,可以按照某一列或多列进行排序。排序功能通常由视图自身实现,也可以通过模型来实现。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTableView *tableView = new QTableView();
    2 tableView->setSortingEnabled(true); // 启用排序功能

    ▮▮▮▮上述代码启用了 QTableView 的排序功能,用户可以通过点击表头来对表格数据进行排序。

    过滤 (Filtering)
    ▮▮▮▮视图支持对数据进行过滤 (filtering),只显示满足特定条件的数据项。过滤功能通常由模型来实现,视图根据模型的过滤结果显示数据。Qt 提供了 QSortFilterProxyModel 类,可以方便地为模型添加排序和过滤功能。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QFileSystemModel *model = new QFileSystemModel();
    2 model->setRootPath(QDir::homePath());
    3
    4 QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel();
    5 proxyModel->setSourceModel(model); // 设置源模型
    6 proxyModel->setFilterWildcard("*.cpp *.h"); // 设置过滤器,只显示 C++ 源文件和头文件
    7
    8 QTreeView *treeView = new QTreeView();
    9 treeView->setModel(proxyModel); // 将代理模型设置给视图
    10 treeView->setRootIndex(proxyModel->mapFromSource(model->index(QDir::homePath())));
    11 treeView->show();

    ▮▮▮▮这段代码创建了一个 QSortFilterProxyModel 代理模型,并将 QFileSystemModel 设置为源模型。然后,设置了过滤器,只显示文件名匹配通配符 *.cpp *.h 的文件和目录。最后,将代理模型设置给 QTreeView,树形视图将只显示 C++ 源文件和头文件。

    通过配置视图的显示属性、选择模式、排序和过滤等选项,可以灵活地定制数据的展示方式和用户交互行为,满足各种应用场景的需求。

    7.4 委托 (Delegate) 的定制

    委托 (Delegate) 负责控制视图中数据项的渲染 (rendering)编辑 (editing) 方式。默认情况下,Qt 视图使用 QStyledItemDelegate 作为默认委托。QStyledItemDelegate 能够处理常见的文本、图标和复选框等数据的显示和编辑。然而,在某些情况下,默认委托可能无法满足需求,例如需要自定义数据项的显示外观,或者需要使用特殊的编辑器来编辑数据。这时就需要创建自定义委托。

    7.4.1 委托的作用:控制数据的显示与编辑

    委托的主要作用有两个:

    控制数据的显示 (Controlling Data Display)
    ▮▮▮▮委托负责绘制视图中的数据项。当视图需要显示模型中的某个数据项时,它会调用委托的 paint() 函数,并将画家 (painter)选项 (option)索引 (index) 等信息传递给委托。委托需要在 paint() 函数中,使用画家在指定的区域内绘制数据项的内容。通过自定义委托的 paint() 函数,可以完全控制数据项的显示外观,例如自定义字体、颜色、背景、边框、图标等,甚至可以绘制复杂的图形和动画效果。

    控制数据的编辑 (Controlling Data Editing)
    ▮▮▮▮当用户想要编辑视图中的某个数据项时,视图会使用委托来提供编辑器 (editor)。视图会调用委托的 createEditor() 函数来创建一个编辑器控件(例如 QLineEdit, QComboBox, QSpinBox 等),并将编辑器控件放置在数据项的位置。然后,视图会调用委托的 setEditorData() 函数,将模型中数据项的值设置给编辑器。当用户完成编辑后,视图会调用委托的 editorDataToModelData() 函数,从编辑器中获取用户输入的值,并将其更新到模型中。通过自定义委托的编辑相关函数,可以定制数据项的编辑方式,例如使用特定的编辑器类型、验证用户输入、格式化数据等。

    7.4.2 默认委托与自定义委托

    Qt 提供了两种委托基类:

    QStyledItemDelegate
    ▮▮▮▮QStyledItemDelegate 是 Qt 提供的默认委托实现。它继承自 QItemDelegate,并使用了 Qt 的样式系统 (style system) 来绘制数据项。QStyledItemDelegate 能够处理常见的文本、图标和复选框等数据的显示和编辑,并且能够根据当前样式 (style) 自动调整数据项的外观。QStyledItemDelegate 适用于大多数应用场景,通常情况下无需自定义委托,直接使用默认委托即可。如果只需要轻微地修改数据项的显示外观,可以通过样式表 (style sheets) 来定制 QStyledItemDelegate

    QItemDelegate
    ▮▮▮▮QItemDelegate 是一个更基础的委托基类。它没有使用样式系统,而是提供了更底层的绘制和编辑接口。如果需要完全自定义数据项的显示外观和编辑行为,或者需要创建特殊的编辑器类型,则需要继承 QItemDelegate 并实现相关的虚函数。自定义 QItemDelegate 的灵活性更高,但也需要更多的开发工作。

    在大多数情况下,QStyledItemDelegate 已经足够满足需求。只有当需要非常特殊的显示效果或编辑行为时,才需要考虑自定义委托,并且通常继承自 QStyledItemDelegate 并重写部分函数即可。只有在极少数情况下,才需要继承 QItemDelegate 从头开始实现委托。

    7.4.3 创建自定义委托:继承 QStyledItemDelegate, QItemDelegate

    创建自定义委托通常需要继承 QStyledItemDelegateQItemDelegate 类,并重写相关的虚函数。以下示例演示了如何创建一个自定义委托,用于在表格视图中显示进度条:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QStyledItemDelegate>
    2 #include <QProgressBar>
    3 #include <QPainter>
    4 #include <QStyleOptionViewItem>
    5 #include <QModelIndex>
    6
    7 class ProgressBarDelegate : public QStyledItemDelegate {
    8 Q_OBJECT
    9 public:
    10 ProgressBarDelegate(QObject *parent = nullptr) : QStyledItemDelegate(parent) {}
    11
    12 void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
    13 if (index.data().canConvert<int>()) {
    14 int progress = index.data().toInt();
    15 if (progress >= 0 && progress <= 100) {
    16 QStyleOptionProgressBar progressBarOption;
    17 progressBarOption.rect = option.rect;
    18 progressBarOption.minimum = 0;
    19 progressBarOption.maximum = 100;
    20 progressBarOption.progress = progress;
    21 progressBarOption.textVisible = true;
    22 progressBarOption.text = QString::number(progress) + "%";
    23 progressBarOption.state = option.state;
    24
    25 QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter);
    26 return; // 如果绘制了进度条,则直接返回,不调用父类的 paint 函数
    27 }
    28 }
    29 QStyledItemDelegate::paint(painter, option, index); // 调用父类的 paint 函数绘制默认内容
    30 }
    31
    32 QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
    33 Q_UNUSED(option);
    34 Q_UNUSED(index);
    35 return new QSpinBox(parent); // 使用 QSpinBox 作为编辑器
    36 }
    37
    38 void setEditorData(QWidget *editor, const QModelIndex &index) const override {
    39 QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
    40 spinBox->setValue(index.data().toInt()); // 将模型数据设置给编辑器
    41 }
    42
    43 void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
    44 QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
    45 spinBox->interpretText(); // 确保文本框的值被解释
    46 model->setData(index, spinBox->value()); // 将编辑器数据更新到模型
    47 }
    48
    49 void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override {
    50 Q_UNUSED(index);
    51 editor->setGeometry(option.rect); // 设置编辑器的几何形状与数据项相同
    52 }
    53 };

    ▮▮▮▮上述代码示例创建了一个名为 ProgressBarDelegate 的自定义委托,它继承自 QStyledItemDelegate。该委托重写了 paint(), createEditor(), setEditorData(), setModelData()updateEditorGeometry() 函数。

    paint() 函数:在该函数中,我们判断数据是否可以转换为整数,并且是否在 0-100 范围内。如果是,则使用 QStyleOptionProgressBarQApplication::style()->drawControl() 绘制一个进度条。否则,调用父类的 paint() 函数绘制默认内容。
    createEditor() 函数:在该函数中,我们创建了一个 QSpinBox 对象作为编辑器。
    setEditorData() 函数:在该函数中,我们将模型数据(进度值)设置给 QSpinBox 编辑器。
    setModelData() 函数:在该函数中,我们从 QSpinBox 编辑器中获取用户输入的值,并将其更新到模型中。
    updateEditorGeometry() 函数:在该函数中,我们将编辑器的几何形状设置为与数据项相同。

    要使用自定义委托,需要将委托对象设置给视图:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTableView *tableView = new QTableView();
    2 ProgressBarDelegate *delegate = new ProgressBarDelegate(tableView); // 创建自定义委托
    3 tableView->setItemDelegateForColumn(1, delegate); // 将委托设置给第二列
    4 tableView->setModel(model); // 设置模型
    5 tableView->show();

    ▮▮▮▮上述代码创建了一个 ProgressBarDelegate 对象,并使用 setItemDelegateForColumn() 函数将其设置给 QTableView 的第二列。这样,表格视图第二列的数据项就会使用进度条来显示,并且可以使用 QSpinBox 进行编辑。

    7.4.4 委托的接口:paint(), createEditor(), setEditorData(), editorDataToModelData()

    委托需要实现一些特定的接口函数,以便视图能够控制数据项的渲染和编辑。最核心的接口函数包括:

    paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
    ▮▮▮▮paint() 函数是委托最核心的接口函数,用于绘制数据项。
    ▮▮▮▮⚝ painter 参数是一个 QPainter 对象,用于进行绘制操作。
    ▮▮▮▮⚝ option 参数是一个 QStyleOptionViewItem 对象,包含了数据项的样式选项 (style options),例如数据项的矩形区域、状态、字体、颜色等。可以通过 option 参数获取数据项的绘制上下文信息。
    ▮▮▮▮⚝ index 参数是一个 QModelIndex 对象,指定要绘制的数据项的索引。

    ▮▮▮▮在 paint() 函数中,需要使用 painter 对象,根据 optionindex 参数,绘制数据项的内容。通常情况下,需要先调用 option.rect 获取数据项的绘制区域,然后使用 painter 的各种绘制函数(例如 drawText(), drawPixmap(), fillRect(), drawLine() 等)在区域内绘制数据项的内容。如果需要绘制复杂的图形或动画效果,可以使用 QPainterPath 和动画框架。

    createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
    ▮▮▮▮createEditor() 函数用于创建编辑器 (editor) 控件。当用户开始编辑数据项时,视图会调用此函数。
    ▮▮▮▮⚝ parent 参数是编辑器的父控件 (parent widget),通常是视图本身。
    ▮▮▮▮⚝ option 参数是一个 QStyleOptionViewItem 对象,与 paint() 函数中的 option 参数相同。
    ▮▮▮▮⚝ index 参数是一个 QModelIndex 对象,指定要编辑的数据项的索引。

    ▮▮▮▮在 createEditor() 函数中,需要创建一个合适的编辑器控件,例如 QLineEdit, QComboBox, QSpinBox 等,并将其返回。编辑器控件的类型取决于要编辑的数据类型和编辑需求。例如,如果需要编辑文本,可以使用 QLineEditQTextEdit。如果需要从列表中选择,可以使用 QComboBoxQListView。如果需要编辑数值,可以使用 QSpinBoxQDoubleSpinBox

    setEditorData(QWidget *editor, const QModelIndex &index) const
    ▮▮▮▮setEditorData() 函数用于将模型数据设置给编辑器 (editor)。在 createEditor() 函数创建编辑器后,视图会调用此函数。
    ▮▮▮▮⚝ editor 参数是 createEditor() 函数创建的编辑器控件。
    ▮▮▮▮⚝ index 参数是一个 QModelIndex 对象,指定要编辑的数据项的索引。

    ▮▮▮▮在 setEditorData() 函数中,需要从模型中获取数据项的值(通常使用 index.model()->data(index, Qt::EditRole)),并将该值设置给编辑器控件。具体的设置方式取决于编辑器控件的类型。例如,对于 QLineEdit,可以使用 setText() 函数设置文本。对于 QSpinBox,可以使用 setValue() 函数设置数值。

    editorDataToModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
    ▮▮▮▮editorDataToModelData() 函数用于将编辑器数据更新到模型 (model)。当用户完成编辑并提交更改时(例如按下 Enter 键或点击其他数据项),视图会调用此函数。
    ▮▮▮▮⚝ editor 参数是 createEditor() 函数创建的编辑器控件。
    ▮▮▮▮⚝ model 参数是视图的模型对象。
    ▮▮▮▮⚝ index 参数是一个 QModelIndex 对象,指定要更新的数据项的索引。

    ▮▮▮▮在 editorDataToModelData() 函数中,需要从编辑器控件中获取用户输入的值,并将该值更新到模型中(通常使用 model->setData(index, editorValue, Qt::EditRole))。具体的获取方式取决于编辑器控件的类型。例如,对于 QLineEdit,可以使用 text() 函数获取文本。对于 QSpinBox,可以使用 value() 函数获取数值。

    updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
    ▮▮▮▮updateEditorGeometry() 函数用于更新编辑器 (editor) 的几何形状。在编辑器显示或视图大小改变时,视图可能会调用此函数。
    ▮▮▮▮⚝ editor 参数是 createEditor() 函数创建的编辑器控件。
    ▮▮▮▮⚝ option 参数是一个 QStyleOptionViewItem 对象,与 paint() 函数中的 option 参数相同。
    ▮▮▮▮⚝ index 参数是一个 QModelIndex 对象,指定要编辑的数据项的索引。

    ▮▮▮▮在 updateEditorGeometry() 函数中,需要将编辑器的几何形状设置为与数据项的绘制区域相同(通常使用 editor->setGeometry(option.rect))。这样可以确保编辑器控件覆盖整个数据项区域,提供良好的用户体验。

    通过实现上述委托接口函数,可以完全控制视图中数据项的渲染和编辑行为,实现高度定制化的数据展示和交互效果。

    8. Qt 多线程:提升程序性能

    章节概要

    本章将深入探讨 Qt 框架中的多线程编程 (Multi-threading Programming) 技术。多线程是现代应用程序开发中不可或缺的一部分,尤其是在图形用户界面 (GUI) 应用中,它对于保持程序的响应性和提升性能至关重要。本章将系统地介绍多线程编程的基础概念,Qt 提供的线程类 QThread 的使用方法,以及 Qt 并发框架 (Concurrency Framework) QtConcurrent。此外,还将详细讲解线程同步与数据保护的关键技术,帮助读者在 Qt GUI 应用中安全高效地使用多线程,从而显著提升程序的用户体验和整体性能。

    8.1 多线程编程基础

    8.1.1 进程 (Process) 与线程 (Thread) 的概念

    在深入多线程编程之前,理解进程 (Process) 和线程 (Thread) 这两个基本概念至关重要。它们是操作系统进行资源分配和任务调度的基本单位。

    进程 (Process)

    ⚝ 进程是操作系统中资源分配的基本单位。当一个程序开始运行时,操作系统会为其创建一个进程。
    ⚝ 每个进程都拥有独立的内存空间系统资源(例如:文件句柄、网络端口等)。这意味着进程之间是相互独立的,一个进程的崩溃通常不会影响到其他进程。
    ⚝ 进程可以包含一个或多个线程。

    线程 (Thread)

    ⚝ 线程是操作系统中任务调度的基本单位,也被称为轻量级进程 (Lightweight Process)。线程存在于进程之中,是进程的实际运作单位。
    ⚝ 同一个进程内的多个线程共享进程的内存空间和系统资源。这使得线程间通信比进程间通信更加高效和便捷。
    ⚝ 多线程允许在一个程序中并发执行多个任务。这对于 GUI 应用程序尤其重要,因为它可以使耗时的操作(例如:网络请求、文件读写、复杂计算等)在后台线程中执行,而不会阻塞主线程(GUI 线程),从而保持界面的流畅响应。

    进程与线程的关系:

    可以用一个比喻来理解进程和线程的关系:可以将进程比作工厂,工厂拥有独立的厂房(内存空间)和设备(系统资源)。而线程则是工厂中的工人,多个工人在同一个工厂内协同工作,共享工厂的资源来完成不同的任务。

    包含关系:线程是进程的一部分,进程包含一个或多个线程。
    资源共享:同一进程内的线程共享进程的内存空间和系统资源,但拥有独立的栈空间和程序计数器。
    调度单位:操作系统调度和管理线程,而不是直接调度进程内的具体任务。

    理解进程和线程的区别与联系是掌握多线程编程的基础。在 Qt GUI 编程中,我们主要关注如何利用多线程技术来提升应用程序的性能和响应性。

    8.1.2 多线程的优势:提高响应性、并发性、资源利用率

    在 GUI 应用程序中使用多线程技术,可以带来多方面的优势,显著提升用户体验和程序性能。

    提高响应性 (Responsiveness)

    ⚝ GUI 应用程序通常是事件驱动的,主线程负责处理用户界面事件(例如:鼠标点击、键盘输入、窗口绘制等)。如果所有任务都在主线程中执行,当执行耗时操作时,主线程会被阻塞,导致界面卡顿,程序失去响应,用户体验极差。
    ⚝ 通过将耗时的操作(例如:网络请求、文件读写、复杂计算等)放到后台线程中执行,主线程可以继续保持运行,及时响应用户操作,从而显著提高 GUI 应用程序的响应性。用户可以流畅地与界面交互,即使后台正在进行耗时任务。

    提高并发性 (Concurrency)

    ⚝ 现代计算机通常配备多核处理器。多线程技术可以充分利用多核处理器的计算能力,实现真正的并行计算
    ⚝ 通过创建多个线程,可以将一个程序中的任务分解并分配到不同的线程上,让它们在多个处理器核心上同时执行,从而缩短程序的整体执行时间,提高程序的并发性能。
    ⚝ 例如,在图像处理应用中,可以将图像分割成多个区域,每个区域由一个线程处理,从而并行加速图像处理过程。

    提高资源利用率 (Resource Utilization)

    ⚝ 在某些情况下,程序可能需要等待外部资源(例如:网络数据、用户输入、文件 I/O 等)。在单线程程序中,程序在等待资源时会被阻塞,处理器处于空闲状态,资源利用率不高。
    ⚝ 多线程程序可以在一个线程等待资源时,切换到其他线程继续执行,充分利用处理器的时间,提高系统的资源利用率。
    ⚝ 例如,在一个网络下载应用中,可以创建一个线程负责下载,另一个线程负责显示下载进度,当下载线程等待网络数据时,显示进度的线程可以继续运行,更新界面。

    简化异步编程 (Simplified Asynchronous Programming)

    ⚝ 多线程提供了一种相对简单的方式来实现异步编程。可以将异步任务放到独立的线程中执行,通过线程间的通信机制(例如:信号与槽)来获取任务结果,避免了传统异步编程中复杂的回调函数或事件处理。
    ⚝ 使用多线程可以使异步代码的逻辑更加清晰和易于维护。

    总而言之,多线程技术是提升 GUI 应用程序性能和用户体验的关键技术之一。合理地使用多线程,可以使程序更加流畅、高效、响应迅速。

    8.1.3 多线程编程的挑战:线程同步、数据竞争、死锁

    虽然多线程编程带来了诸多优势,但也引入了一些新的挑战。开发者需要认真对待这些挑战,才能编写出安全、稳定、高效的多线程程序。

    线程同步 (Thread Synchronization)

    ⚝ 当多个线程共享访问相同的资源(例如:内存中的数据、文件、网络连接等)时,需要进行线程同步,以保证数据的一致性和完整性。
    竞态条件 (Race Condition):如果没有进行正确的线程同步,可能会出现竞态条件。竞态条件指的是程序的执行结果依赖于线程执行的相对顺序,而这种顺序在多线程环境下是不确定的。这会导致程序出现不可预测的错误。
    同步机制:Qt 提供了多种线程同步机制,例如:互斥锁 (Mutex)读写锁 (Read-Write Lock)条件变量 (Condition Variable)原子操作 (Atomic Operations) 等。开发者需要根据具体的场景选择合适的同步机制,来协调线程对共享资源的访问。

    数据竞争 (Data Race)

    ⚝ 数据竞争是一种特殊的竞态条件,指当多个线程同时访问同一块内存,并且至少有一个线程进行写操作时,如果没有适当的同步机制,就会发生数据竞争。
    ⚝ 数据竞争会导致未定义行为 (Undefined Behavior),程序可能会崩溃、数据损坏、或者产生难以调试的错误。
    避免数据竞争:避免数据竞争的关键在于保护共享数据。可以使用互斥锁等同步机制,保证在同一时刻只有一个线程可以访问或修改共享数据。

    死锁 (Deadlock)

    ⚝ 死锁是指多个线程相互等待对方释放资源,导致所有线程都无法继续执行的僵局状态。
    死锁的产生条件:死锁通常发生在以下四个条件同时满足时:
    ▮▮▮▮⚝ 互斥条件 (Mutual Exclusion):资源是独占的,同一时刻只能被一个线程占用。
    ▮▮▮▮⚝ 占有并等待条件 (Hold and Wait):线程已经占有了一些资源,但又请求新的资源,并且在等待新资源的过程中,不会释放已占有的资源。
    ▮▮▮▮⚝ 不可剥夺条件 (No Preemption):线程已占有的资源,在未使用完之前,不能被其他线程强行剥夺。
    ▮▮▮▮⚝ 循环等待条件 (Circular Wait):多个线程之间形成循环等待资源的关系。
    避免死锁:避免死锁的关键在于破坏死锁产生的必要条件。例如,可以避免循环等待条件,或者使用超时机制来避免线程无限期等待。

    上下文切换开销 (Context Switching Overhead)

    ⚝ 多线程的并发执行是通过操作系统快速切换线程来实现的,这种切换称为上下文切换 (Context Switching)。上下文切换本身会带来一定的开销,包括保存和恢复线程的上下文信息、调度器的开销等。
    ⚝ 如果线程数量过多,或者线程切换过于频繁,上下文切换的开销可能会超过多线程带来的性能提升,甚至导致程序性能下降。
    合理控制线程数量:需要根据具体的应用场景和硬件环境,合理控制线程数量,避免过度使用线程。

    调试困难 (Debugging Difficulty)

    ⚝ 多线程程序的调试通常比单线程程序更加困难。线程的并发执行和不确定性,使得 Bug 更难复现和定位。
    调试工具:可以使用调试器(例如:GDB、LLDB、Visual Studio Debugger 等)提供的多线程调试功能,例如:线程查看、断点设置、单步执行等,来辅助多线程程序的调试。
    日志 (Logging):合理的日志输出也是多线程程序调试的重要手段。可以在关键代码段添加日志输出,记录线程的执行状态和数据变化,帮助分析问题。

    总之,多线程编程虽然强大,但也需要开发者具备扎实的编程基础和深入的理解。只有充分认识到多线程编程的挑战,并掌握相应的技术和方法,才能编写出高质量的多线程应用程序。

    8.2 Qt 的线程类:QThread

    Qt 框架提供了 QThread 类,用于进行多线程编程。QThread 封装了线程的创建、启动、停止、生命周期管理等操作,为开发者提供了便捷的线程编程接口。

    8.2.1 创建线程:继承 QThread, 重写 run() 函数

    在 Qt 中,创建线程最常用的方式是继承 QThread,并重写 run() 函数run() 函数是线程的入口函数,线程启动后,操作系统会调用 run() 函数来执行线程的任务。

    创建自定义线程类

    首先,需要创建一个类,继承自 QThread。例如,创建一个名为 MyThread 的线程类:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QThread>
    2 #include <QDebug>
    3
    4 class MyThread : public QThread {
    5 Q_OBJECT // 启用 Qt 元对象系统特性
    6
    7 public:
    8 MyThread(QObject *parent = nullptr) : QThread(parent) {}
    9
    10 protected:
    11 void run() override {
    12 // 线程要执行的任务代码
    13 qDebug() << "Thread started: " << QThread::currentThreadId();
    14 // ... 耗时操作 ...
    15 qDebug() << "Thread finished: " << QThread::currentThreadId();
    16 }
    17 };

    Q_OBJECT: 必须在类声明中添加 Q_OBJECT 宏,以启用 Qt 的元对象系统 (Meta-Object System) 特性,例如:信号与槽 (Signals and Slots)。
    构造函数: 构造函数可以接受一个 QObject 类型的父对象指针 parent,用于设置线程的父对象。Qt 的对象树 (Object Tree) 机制会自动管理线程对象的生命周期。
    run() 函数: 必须重写 run() 函数。线程要执行的任务代码应该放在 run() 函数中。在上面的例子中, run() 函数只是简单地输出了线程的启动和结束信息。实际应用中,可以在 run() 函数中执行耗时的操作,例如:网络请求、文件读写、复杂计算等。
    QThread::currentThreadId()QThread::currentThreadId() 函数可以获取当前线程的 ID。

    实现线程任务

    run() 函数中编写线程要执行的具体任务代码。例如,模拟一个耗时操作,可以使用 QThread::sleep()QThread::msleep() 函数让线程休眠一段时间:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyThread::run() override {
    2 qDebug() << "Thread started: " << QThread::currentThreadId();
    3 for (int i = 0; i < 5; ++i) {
    4 qDebug() << "Thread is working: " << i;
    5 QThread::msleep(1000); // 线程休眠 1 秒
    6 }
    7 qDebug() << "Thread finished: " << QThread::currentThreadId();
    8 }

    QThread::sleep(seconds): 让线程休眠指定的秒数。
    QThread::msleep(milliseconds): 让线程休眠指定的毫秒数。
    QThread::usleep(microseconds): 让线程休眠指定的微秒数。

    注意不要在 run() 函数中执行 GUI 操作。GUI 操作(例如:创建控件、修改控件属性、更新界面等)必须在主线程 (GUI 线程) 中执行。如果在后台线程中直接操作 GUI,可能会导致程序崩溃或界面显示错误,因为 GUI 控件不是线程安全的 (Thread-safe)。线程间通信可以使用信号与槽机制,将后台线程的结果传递给主线程,由主线程来更新 GUI。

    8.2.2 启动线程:start(), 线程的生命周期管理

    创建了自定义线程类后,需要创建线程对象并启动线程。

    创建线程对象

    在需要使用线程的地方,创建 MyThread 类的对象:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 MyThread *thread = new MyThread();

    ⚝ 使用 new 关键字在堆 (Heap) 上创建线程对象。通常情况下,建议在堆上创建 QThread 对象,以便更好地管理线程的生命周期。

    启动线程

    使用 QThread::start() 函数启动线程。start() 函数会创建一个新的线程,并调用线程对象的 run() 函数开始执行线程任务。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 thread->start();

    QThread::start(): 启动线程。调用 start() 函数后,操作系统会创建一个新的线程,并开始执行线程对象的 run() 函数。start() 函数本身会立即返回,不会阻塞调用线程。
    线程的异步执行: 线程的执行是异步的。调用 start() 函数后,线程会在后台独立运行,不会阻塞调用线程的执行。

    等待线程结束

    如果需要等待线程执行结束后再继续执行后续操作,可以使用 QThread::wait() 函数。 wait() 函数会阻塞调用线程,直到线程执行结束。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 thread->start();
    2 // ... 执行其他操作 ...
    3 thread->wait(); // 等待线程结束
    4 qDebug() << "Thread has finished.";

    QThread::wait(unsigned long time = ULONG_MAX): 阻塞调用线程,直到线程执行结束,或者等待超时。 time 参数指定等待超时时间,单位是毫秒。默认值为 ULONG_MAX,表示无限期等待。
    返回值wait() 函数返回 true 表示线程已成功结束,返回 false 表示等待超时或发生错误。

    线程的生命周期管理

    启动 (Starting): 通过 start() 函数启动线程。
    运行 (Running): 线程执行 run() 函数中的任务。
    结束 (Finished)run() 函数执行完毕后,线程结束。Qt 会自动发出 finished() 信号。
    终止 (Terminated): 可以使用 QThread::terminate() 函数强制终止线程的执行。 不推荐使用 terminate() 函数,因为它可能会导致资源泄露或数据损坏。更安全的终止线程的方法是在 run() 函数中实现线程终止的逻辑,例如:检查一个标志位,当标志位被设置时,让 run() 函数正常退出。
    删除 (Deleting): 当线程结束后,需要手动删除在堆上创建的线程对象,释放内存资源。可以使用 delete 关键字删除线程对象。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 thread->start();
    2 thread->wait();
    3 delete thread; // 删除线程对象
    4 thread = nullptr;

    QThread::finished() 信号: 当线程的 run() 函数执行完毕,线程即将结束时, QThread 会自动发出 finished() 信号。可以连接 finished() 信号到一个槽函数,在槽函数中执行线程结束后的清理操作,例如:删除线程对象。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyObject : public QObject {
    2 Q_OBJECT
    3 public slots:
    4 void onThreadFinished() {
    5 MyThread *thread = qobject_cast<MyThread*>(sender()); // 获取发出信号的线程对象
    6 if (thread) {
    7 qDebug() << "Thread finished signal received.";
    8 delete thread; // 删除线程对象
    9 thread = nullptr;
    10 }
    11 }
    12 };
    13
    14 // ...
    15
    16 MyThread *thread = new MyThread();
    17 MyObject *object = new MyObject();
    18 QObject::connect(thread, &MyThread::finished, object, &MyObject::onThreadFinished); // 连接 finished 信号到槽函数
    19 thread->start();

    父对象机制: 可以为 QThread 对象设置父对象。当父对象被删除时,Qt 的对象树机制会自动删除其所有子对象,包括线程对象。这可以简化线程对象的生命周期管理。

    8.2.3 线程间的通信:信号与槽、事件队列

    在多线程编程中,线程间通信 (Inter-thread Communication) 是一个重要的问题。尤其是在 GUI 应用程序中,后台线程需要将计算结果或任务状态传递给主线程,由主线程来更新 GUI 界面。Qt 提供了多种线程间通信机制,其中最常用的是信号与槽 (Signals and Slots)事件队列 (Event Queue)

    信号与槽 (Signals and Slots)

    ⚝ Qt 的信号与槽机制是一种强大的对象间通信机制,也适用于线程间通信。
    ⚝ 可以将后台线程的信号连接到主线程的槽函数,当后台线程发出信号时,主线程的槽函数会在主线程的事件循环 (Event Loop) 中被调用。
    跨线程信号与槽连接: 当信号和槽位于不同的线程时,Qt 会自动进行跨线程调用。Qt 会将信号的参数数据拷贝到主线程的事件队列中,然后在主线程的事件循环中调用槽函数。这种机制保证了槽函数始终在槽函数所在线程中执行,避免了线程安全问题。
    连接类型: 跨线程信号与槽连接默认使用队列连接 (Queued Connection) 类型。也可以显式指定连接类型为 Qt::QueuedConnection

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyThread : public QThread {
    2 Q_OBJECT
    3 signals:
    4 void resultReady(const QString &result); // 定义信号,传递字符串结果
    5
    6 public:
    7 MyThread(QObject *parent = nullptr) : QThread(parent) {}
    8
    9 protected:
    10 void run() override {
    11 QString result = doWork(); // 执行耗时操作,获取结果
    12 emit resultReady(result); // 发射信号,传递结果
    13 }
    14
    15 private:
    16 QString doWork() {
    17 // ... 耗时操作,例如:网络请求、复杂计算 ...
    18 QThread::sleep(2); // 模拟耗时 2 秒
    19 return "Task completed successfully!";
    20 }
    21 };
    22
    23 class MyWidget : public QWidget {
    24 Q_OBJECT
    25 public:
    26 MyWidget(QWidget *parent = nullptr) : QWidget(parent) {
    27 thread = new MyThread(this); // 创建线程对象,设置父对象为 MyWidget
    28 connect(thread, &MyThread::resultReady, this, &MyWidget::onResultReady); // 连接信号到槽函数
    29 }
    30
    31 ~MyWidget() override {
    32 thread->quit(); // 安全退出线程的事件循环
    33 thread->wait(); // 等待线程结束
    34 }
    35
    36 public slots:
    37 void onResultReady(const QString &result) {
    38 // 在主线程中处理结果,更新 GUI 界面
    39 qDebug() << "Result received in main thread: " << result;
    40 // ... 更新 GUI 界面,例如:显示结果到 QLabel ...
    41 }
    42
    43 void startTask() {
    44 thread->start(); // 启动线程
    45 }
    46
    47 private:
    48 MyThread *thread;
    49 };

    定义信号: 在线程类 MyThread 中定义一个信号 resultReady(const QString &result),用于传递任务结果。
    发射信号: 在 run() 函数中,当任务完成后,使用 emit resultReady(result) 发射信号,并将结果作为信号的参数传递出去。
    连接信号与槽: 在主线程的 MyWidget 类中,使用 connect() 函数将线程的 resultReady 信号连接到 MyWidget 类的槽函数 onResultReady
    在槽函数中处理结果onResultReady 槽函数会在主线程中被调用,可以在槽函数中安全地更新 GUI 界面。

    事件队列 (Event Queue)

    ⚝ Qt 的事件队列机制是 GUI 应用程序的核心机制。每个线程都可以拥有自己的事件队列。
    ⚝ 可以使用 QCoreApplication::postEvent() 函数将自定义事件投递到指定对象的事件队列中。
    ⚝ 接收事件的对象需要重写 event() 函数来处理自定义事件。
    ⚝ 事件队列机制也适用于线程间通信。可以将后台线程需要传递给主线程的数据封装成自定义事件,然后将事件投递到主线程的事件队列中,由主线程的事件循环来处理事件。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 自定义事件类
    2 class MyEvent : public QEvent {
    3 public:
    4 MyEvent(const QString &result) : QEvent(QEvent::Type(QEvent::User + 1)), m_result(result) {} // 使用自定义事件类型
    5 QString result() const { return m_result; }
    6
    7 private:
    8 QString m_result;
    9 };
    10
    11 class MyThread : public QThread {
    12 public:
    13 MyThread(QObject *receiver) : m_receiver(receiver) {} // 接收事件的目标对象
    14
    15 protected:
    16 void run() override {
    17 QString result = doWork();
    18 QCoreApplication::postEvent(m_receiver, new MyEvent(result)); // 投递自定义事件到目标对象的事件队列
    19 }
    20
    21 private:
    22 QString doWork() {
    23 // ... 耗时操作 ...
    24 QThread::sleep(2);
    25 return "Task completed via event!";
    26 }
    27
    28 private:
    29 QObject *m_receiver; // 事件接收者
    30 };
    31
    32 class MyWidget : public QWidget {
    33 protected:
    34 bool event(QEvent *event) override {
    35 if (event->type() == QEvent::Type(QEvent::User + 1)) { // 检查自定义事件类型
    36 MyEvent *myEvent = static_cast<MyEvent*>(event);
    37 onMyEvent(myEvent); // 处理自定义事件
    38 return true; // 事件已被处理
    39 }
    40 return QWidget::event(event); // 调用父类的事件处理函数
    41 }
    42
    43 public slots:
    44 void onMyEvent(MyEvent *event) {
    45 QString result = event->result();
    46 qDebug() << "Result received via event in main thread: " << result;
    47 // ... 更新 GUI 界面 ...
    48 event->accept(); // 标记事件已被处理
    49 }
    50
    51 void startTask() {
    52 thread = new MyThread(this); // 传递 MyWidget 对象作为事件接收者
    53 thread->start();
    54 }
    55
    56 private:
    57 MyThread *thread;
    58 };

    自定义事件类: 创建自定义事件类 MyEvent,继承自 QEvent,并定义自定义的事件类型 QEvent::User + 1。事件类中可以包含需要传递的数据,例如: m_result 字符串。
    投递事件: 在后台线程 MyThreadrun() 函数中,使用 QCoreApplication::postEvent(m_receiver, new MyEvent(result)) 函数将 MyEvent 事件投递到目标对象 m_receiver (这里是 MyWidget 对象)的事件队列中。
    重写 event() 函数: 在 MyWidget 类中,重写 event() 函数,用于接收和处理投递过来的事件。在 event() 函数中,检查事件类型是否为自定义事件类型 QEvent::User + 1,如果是,则将事件强制转换为 MyEvent 类型,并调用 onMyEvent() 槽函数处理事件。
    处理自定义事件onMyEvent() 槽函数会在主线程的事件循环中被调用,可以在槽函数中安全地更新 GUI 界面。

    选择线程间通信机制

    信号与槽: 更简洁、方便、类型安全,是 Qt 中推荐的线程间通信方式。适用于大多数场景。
    事件队列: 更底层、更灵活,可以处理更复杂的事件传递和处理逻辑。适用于需要自定义事件类型和事件处理流程的场景。

    在实际开发中,通常优先选择信号与槽进行线程间通信。只有在信号与槽无法满足需求的情况下,才考虑使用事件队列。

    8.3 Qt 并发框架 (Concurrency Framework):QtConcurrent

    QtConcurrent 模块是 Qt 提供的并发框架,它基于线程池 (Thread Pool) 实现了函数式编程风格的并发操作。QtConcurrent 提供了更高级别的并发编程接口,可以简化并发代码的编写,提高开发效率。

    8.3.1 QtConcurrent 概述:函数式编程风格的并发

    QtConcurrent 框架的核心思想是函数式编程。它将并发操作抽象为对函数数据集合的操作,开发者只需要关注要执行的函数和数据,而无需手动管理线程的创建、启动、同步等细节。QtConcurrent 会自动将任务分配到线程池中的线程中执行,并管理线程的生命周期。

    函数式编程风格

    高阶函数 (Higher-order Functions): QtConcurrent 提供了类似函数式编程中的高阶函数,例如: QtConcurrent::run(), QtConcurrent::map(), QtConcurrent::filter(), QtConcurrent::reduce() 等。这些函数接受函数对象 (Function Object)Lambda 表达式 (Lambda Expression) 作为参数,并将这些函数应用到数据集合上,实现并发操作。
    数据集合操作: QtConcurrent 主要用于对数据集合(例如: QList, QVector, QSet 等)进行并发操作。可以将一个函数应用到数据集合中的每个元素上,实现并行处理。
    声明式编程 (Declarative Programming): 使用 QtConcurrent 编程时,只需要声明要执行的操作和数据,而无需命令式地编写线程管理代码。QtConcurrent 框架会负责底层的线程管理和任务调度。

    线程池 (Thread Pool)

    ⚝ QtConcurrent 内部使用线程池来管理线程。线程池是一组预先创建好的线程,可以复用线程,避免了频繁创建和销毁线程的开销,提高了线程的利用率和程序的性能。
    ⚝ QtConcurrent 的线程池大小会根据系统 CPU 核心数自动调整,以充分利用多核处理器的计算能力。
    ⚝ 开发者无需手动创建和管理线程池,QtConcurrent 会自动处理线程池的创建、管理和任务分配。

    异步操作 (Asynchronous Operations)

    ⚝ QtConcurrent 提供的函数都是异步的。调用 QtConcurrent 函数后,函数会立即返回,不会阻塞调用线程。并发任务会在后台线程中执行。
    ⚝ QtConcurrent 提供了多种方式来获取异步操作的结果,例如:QFuture 对象信号与槽等。

    优点

    简化并发编程: QtConcurrent 简化了并发编程的复杂性,开发者无需手动管理线程,只需要关注任务逻辑。
    提高开发效率: 使用 QtConcurrent 可以快速编写出高效的并发代码,提高开发效率。
    性能优化: QtConcurrent 基于线程池实现,可以充分利用多核处理器的计算能力,并优化线程管理,提高程序性能。
    易于使用: QtConcurrent 的 API 设计简洁易用,容易学习和掌握。

    适用场景

    批量数据处理: 例如:图像处理、数据分析、科学计算等,需要对大量数据进行并行处理的场景。
    CPU 密集型任务: 例如:复杂计算、编码解码、数据压缩解压缩等,需要大量 CPU 计算资源的任务。
    后台任务: 例如:文件批量处理、网络数据下载、数据库批量操作等,需要在后台线程异步执行的任务。

    8.3.2 QtConcurrent::run():在线程池中运行函数

    QtConcurrent::run() 函数是最基本的 QtConcurrent 函数,它可以在线程池中异步运行一个函数

    函数签名

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QFuture<T> QtConcurrent::run(Function function, Arg1 arg1, Arg2 arg2, ...);

    QFuture<T>: 返回值是一个 QFuture<T> 对象。 QFuture<T> 是一个期物 (Future) 对象,用于表示异步操作的结果。 T 是函数 function 的返回值类型。
    Function function: 要在线程池中运行的函数或函数对象。 function 可以是:
    ▮▮▮▮⚝ 普通函数指针 (Function Pointer)
    ▮▮▮▮⚝ Lambda 表达式 (Lambda Expression)
    ▮▮▮▮⚝ 函数对象 (Function Object) (实现了 operator() 的类对象)
    Arg1 arg1, Arg2 arg2, ...: 传递给函数 function 的参数。最多可以传递 7 个参数。

    使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtConcurrent>
    2 #include <QDebug>
    3
    4 QString doLongTask(const QString &input) {
    5 qDebug() << "Task started in thread: " << QThread::currentThreadId();
    6 QThread::sleep(2); // 模拟耗时操作
    7 QString result = "Processed: " + input;
    8 qDebug() << "Task finished in thread: " << QThread::currentThreadId();
    9 return result;
    10 }
    11
    12 void testQtConcurrentRun() {
    13 QString inputData = "Hello QtConcurrent!";
    14 QFuture<QString> future = QtConcurrent::run(doLongTask, inputData); // 异步运行 doLongTask 函数
    15
    16 qDebug() << "Main thread continues to execute: " << QThread::currentThreadId();
    17 // ... 执行其他操作 ...
    18
    19 QString result = future.result(); // 获取异步操作的结果,会阻塞直到结果可用
    20 qDebug() << "Task result received in main thread: " << result;
    21 }

    QtConcurrent::run(doLongTask, inputData): 调用 QtConcurrent::run() 函数,异步运行 doLongTask 函数,并将 inputData 作为参数传递给 doLongTask 函数。
    QFuture<QString> futureQtConcurrent::run() 函数返回一个 QFuture<QString> 对象 future,用于表示异步操作的结果。
    异步执行: 调用 QtConcurrent::run() 函数后, doLongTask 函数会在线程池中的一个线程中异步执行。 testQtConcurrentRun() 函数会立即返回,不会阻塞主线程。
    future.result(): 使用 future.result() 函数可以获取异步操作的结果result() 函数会阻塞调用线程,直到异步操作完成并返回结果。
    Lambda 表达式: 可以使用 Lambda 表达式作为 QtConcurrent::run() 的参数,简化代码:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void testQtConcurrentRunLambda() {
    2 QString inputData = "Hello Lambda!";
    3 QFuture<QString> future = QtConcurrent::run([inputData]() { // 使用 Lambda 表达式
    4 qDebug() << "Lambda task started in thread: " << QThread::currentThreadId();
    5 QThread::sleep(2);
    6 QString result = "Lambda processed: " + inputData;
    7 qDebug() << "Lambda task finished in thread: " << QThread::currentThreadId();
    8 return result;
    9 });
    10
    11 // ...
    12
    13 QString result = future.result();
    14 qDebug() << "Lambda task result: " << result;
    15 }

    无返回值函数: 如果要在线程池中运行的函数没有返回值(void 类型),可以使用 QtConcurrent::run() 函数,并将 QFuture<void> 作为返回值类型:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void doVoidTask() {
    2 qDebug() << "Void task started in thread: " << QThread::currentThreadId();
    3 QThread::sleep(1);
    4 qDebug() << "Void task finished in thread: " << QThread::currentThreadId();
    5 }
    6
    7 void testQtConcurrentRunVoid() {
    8 QFuture<void> future = QtConcurrent::run(doVoidTask); // 异步运行 void 函数
    9
    10 // ...
    11
    12 future.waitForFinished(); // 等待异步操作完成,但不获取结果
    13 qDebug() << "Void task finished.";
    14 }

    future.waitForFinished()QFuture::waitForFinished() 函数会阻塞调用线程,直到异步操作完成。与 result() 函数不同, waitForFinished() 函数不获取异步操作的结果,只等待操作完成。

    8.3.3 QtConcurrent::map(), QtConcurrent::filter(), QtConcurrent::reduce():并行算法

    QtConcurrent 提供了 QtConcurrent::map(), QtConcurrent::filter(), QtConcurrent::reduce()并行算法,用于对数据集合进行并行处理。这些算法类似于 C++ 标准库中的 std::transform, std::remove_if, std::accumulate 等算法,但 QtConcurrent 的算法是并行执行的,可以充分利用多核处理器的计算能力。

    QtConcurrent::map()

    功能: 将一个函数映射 (map) 到数据集合中的每个元素上,并将结果收集到一个新的数据集合中。类似于 std::transform 算法的并行版本。
    函数签名

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QFuture<T> QtConcurrent::map(InputIterator begin, InputIterator end, MapFunction function);
    2 QFuture<T> QtConcurrent::map(Sequence &sequence, MapFunction function);

    ▮▮▮▮⚝ InputIterator begin, InputIterator end: 迭代器范围,指定要处理的数据集合的起始和结束位置。
    ▮▮▮▮⚝ Sequence &sequence: 数据集合序列,例如: QList, QVector 等。
    ▮▮▮▮⚝ MapFunction function: 映射函数,接受数据集合中的一个元素作为参数,并返回处理后的结果。

    使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtConcurrent>
    2 #include <QDebug>
    3
    4 int square(int number) {
    5 qDebug() << "Square task for " << number << " in thread: " << QThread::currentThreadId();
    6 QThread::msleep(500); // 模拟计算耗时
    7 return number * number;
    8 }
    9
    10 void testQtConcurrentMap() {
    11 QList<int> numbers = {1, 2, 3, 4, 5};
    12 QFuture<int> future = QtConcurrent::map(numbers, square); // 并行计算 numbers 列表中每个元素的平方
    13
    14 qDebug() << "Main thread continues...";
    15
    16 future.waitForFinished(); // 等待 map 操作完成
    17 QList<int> squaredNumbers = future.results(); // 获取 map 操作的结果列表
    18
    19 qDebug() << "Squared numbers: " << squaredNumbers; // 输出平方后的结果
    20 }

    QtConcurrent::map(numbers, square): 调用 QtConcurrent::map() 函数,并行地将 square 函数映射到 numbers 列表中的每个元素上。
    future.results()QFuture::results() 函数返回一个 QList<int>,包含了 map 操作的结果列表,结果顺序与输入数据顺序一致。

    QtConcurrent::filter()

    功能过滤 (filter) 数据集合中的元素,根据指定的过滤条件,只保留满足条件的元素,并将结果收集到一个新的数据集合中。类似于 std::remove_if 算法的反向并行版本。
    函数签名

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QFuture<T> QtConcurrent::filter(InputIterator begin, InputIterator end, FilterFunction function);
    2 QFuture<T> QtConcurrent::filter(Sequence &sequence, FilterFunction function);

    ▮▮▮▮⚝ FilterFunction function: 过滤函数,接受数据集合中的一个元素作为参数,并返回 bool 值。 true 表示保留该元素, false 表示过滤掉该元素。

    使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 bool isEven(int number) {
    2 qDebug() << "IsEven check for " << number << " in thread: " << QThread::currentThreadId();
    3 QThread::msleep(200); // 模拟判断耗时
    4 return number % 2 == 0;
    5 }
    6
    7 void testQtConcurrentFilter() {
    8 QList<int> numbers = {1, 2, 3, 4, 5, 6};
    9 QFuture<int> future = QtConcurrent::filter(numbers, isEven); // 并行过滤 numbers 列表中的偶数
    10
    11 future.waitForFinished();
    12 QList<int> evenNumbers = future.results();
    13
    14 qDebug() << "Even numbers: " << evenNumbers; // 输出过滤后的偶数列表
    15 }

    QtConcurrent::filter(numbers, isEven): 调用 QtConcurrent::filter() 函数,并行地使用 isEven 函数过滤 numbers 列表中的元素,只保留偶数。

    QtConcurrent::reduce()

    功能: 对数据集合中的元素进行归约 (reduce) 操作,将数据集合聚合为一个单一的值。类似于 std::accumulate 算法的并行版本。
    函数签名

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QFuture<T> QtConcurrent::reduce(InputIterator begin, InputIterator end, ReduceFunction function, ReduceArgumentType initValue);
    2 QFuture<T> QtConcurrent::reduce(Sequence &sequence, ReduceFunction function, ReduceArgumentType initValue);

    ▮▮▮▮⚝ ReduceFunction function: 归约函数,接受两个参数:
    ▮▮▮▮▮▮▮▮⚝ ReduceArgumentType &result: 累积结果的引用。
    ▮▮▮▮▮▮▮▮⚝ const T &nextValue: 数据集合中的下一个元素。
    ▮▮▮▮▮▮▮▮⚝ ReduceFunction 函数负责将 nextValue 累积到 result 中。
    ▮▮▮▮⚝ ReduceArgumentType initValue: 归约操作的初始值

    使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void sumReducer(int &sum, int number) {
    2 qDebug() << "Sum reducer for " << number << " in thread: " << QThread::currentThreadId();
    3 QThread::msleep(100); // 模拟累加耗时
    4 sum += number; // 将 number 累加到 sum 中
    5 }
    6
    7 void testQtConcurrentReduce() {
    8 QList<int> numbers = {1, 2, 3, 4, 5};
    9 int initialSum = 0;
    10 QFuture<int> future = QtConcurrent::reduce(numbers, sumReducer, initialSum); // 并行计算 numbers 列表元素的总和
    11
    12 future.waitForFinished();
    13 int totalSum = future.result(); // 获取归约操作的结果,即总和
    14
    15 qDebug() << "Total sum: " << totalSum; // 输出总和
    16 }

    QtConcurrent::reduce(numbers, sumReducer, initialSum): 调用 QtConcurrent::reduce() 函数,并行地使用 sumReducer 函数对 numbers 列表中的元素进行归约操作,初始值为 initialSum (0)。

    QtConcurrent::map-reduce 组合

    可以将 QtConcurrent::map()QtConcurrent::reduce() 组合使用,实现更复杂的并行数据处理流程。例如,先使用 map() 函数对数据进行并行转换,然后使用 reduce() 函数将转换后的数据聚合为一个结果。

    注意

    线程安全: 传递给 QtConcurrent 并行算法的函数(例如: MapFunction, FilterFunction, ReduceFunction )必须是线程安全的。这意味着这些函数不能访问共享的可变状态,或者必须使用适当的同步机制来保护共享状态。
    性能考虑: 并行算法的性能提升取决于任务的计算量、数据集合的大小、以及硬件环境(CPU 核心数)。对于计算量较小的任务,并行执行的开销可能会超过收益,导致性能下降。需要根据实际情况进行性能测试和优化。

    8.4 线程同步与数据保护

    在多线程编程中,线程同步 (Thread Synchronization) 和数据保护 (Data Protection) 是至关重要的。当多个线程共享访问同一份数据时,必须采取适当的同步机制,防止数据竞争 (Data Race) 和其他并发问题,保证程序的正确性和稳定性。Qt 提供了多种线程同步机制,常用的包括:互斥锁 (Mutex)、读写锁 (Read-Write Lock)、条件变量 (Condition Variable)、原子操作 (Atomic Operations) 等。

    8.4.1 互斥锁 (Mutex):QMutex, QMutexLocker

    互斥锁 (Mutex),全称 Mutual Exclusion Lock,是最基本的线程同步机制之一。互斥锁用于保护临界区 (Critical Section),保证在同一时刻只有一个线程可以访问临界区中的共享资源。

    互斥锁的概念

    ⚝ 互斥锁可以看作是一个,用于保护共享资源。
    ⚝ 线程在访问共享资源之前,需要先获取锁 (Lock)。如果锁已经被其他线程占用,则当前线程会被阻塞 (Block),直到锁被释放。
    ⚝ 当线程完成对共享资源的访问后,需要释放锁 (Unlock),以便其他线程可以获取锁并访问资源。
    互斥性 (Mutual Exclusion): 互斥锁保证了在任何时刻,最多只有一个线程可以持有锁,从而实现对共享资源的互斥访问。

    QMutex

    Qt 提供了 QMutex 类来实现互斥锁。 QMutex 提供了以下主要方法:

    QMutex::lock(): 获取互斥锁。如果锁已被其他线程占用,则当前线程会被阻塞,直到锁被释放。
    QMutex::unlock(): 释放互斥锁。释放锁后,其他等待锁的线程可以尝试获取锁。
    QMutex::tryLock(int timeout = 0): 尝试获取互斥锁。如果锁在指定的 timeout 毫秒内(默认值为 0,表示立即返回)没有被获取到,则函数返回 false,否则返回 truetryLock() 函数不会阻塞线程。

    QMutexLocker

    为了更方便、更安全地使用互斥锁,Qt 提供了 QMutexLocker 类。 QMutexLocker 是一个RAII (Resource Acquisition Is Initialization) 风格的互斥锁包装类。

    RAII 机制: RAII 机制保证了资源的自动管理。当 QMutexLocker 对象被创建时,它会自动获取互斥锁。当 QMutexLocker 对象超出作用域 (Scope) 或被销毁时,它会自动释放互斥锁。
    异常安全 (Exception Safety): 使用 QMutexLocker 可以保证即使在临界区代码中发生异常,互斥锁也能被正确释放,避免死锁。

    使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QMutex>
    2 #include <QMutexLocker>
    3 #include <QDebug>
    4
    5 int sharedCounter = 0;
    6 QMutex counterMutex; // 定义互斥锁
    7
    8 void incrementCounter() {
    9 for (int i = 0; i < 10000; ++i) {
    10 QMutexLocker locker(&counterMutex); // 创建 QMutexLocker 对象,自动获取互斥锁
    11 // 临界区开始:保护 sharedCounter 的访问
    12 sharedCounter++; // 访问共享变量 sharedCounter
    13 // 临界区结束:locker 对象超出作用域,自动释放互斥锁
    14 }
    15 }
    16
    17 void testMutex() {
    18 QThread thread1, thread2;
    19 QObject::connect(&thread1, &QThread::started, [](){ incrementCounter(); });
    20 QObject::connect(&thread2, &QThread::started, [](){ incrementCounter(); });
    21
    22 thread1.start();
    23 thread2.start();
    24
    25 thread1.wait();
    26 thread2.wait();
    27
    28 qDebug() << "Shared counter value: " << sharedCounter; // 输出最终的计数器值,应为 20000
    29 }

    QMutex counterMutex: 定义一个全局的互斥锁对象 counterMutex,用于保护共享变量 sharedCounter
    QMutexLocker locker(&counterMutex): 在 incrementCounter() 函数中,创建一个 QMutexLocker 对象 locker,并将互斥锁 counterMutex 的地址传递给 QMutexLocker 的构造函数。 QMutexLocker 对象创建时,会自动调用 counterMutex.lock() 获取互斥锁。
    临界区: 在 QMutexLocker 对象的作用域内({ ... } 括起来的代码块),就是临界区。在临界区中,可以安全地访问共享变量 sharedCounter,因为互斥锁保证了同一时刻只有一个线程可以进入临界区。
    自动释放锁: 当 QMutexLocker 对象 locker 超出作用域时,其析构函数会被调用,析构函数会自动调用 counterMutex.unlock() 释放互斥锁。

    死锁风险

    ⚝ 使用互斥锁时,需要注意死锁的风险。如果多个线程循环等待对方释放互斥锁,就会发生死锁。
    避免嵌套锁: 尽量避免在一个线程中嵌套获取多个互斥锁。如果必须嵌套获取多个锁,要保证锁的获取顺序一致,避免循环等待。
    超时机制: 可以使用 QMutex::tryLock(timeout) 函数,尝试在指定时间内获取锁。如果超时未获取到锁,则放弃获取,避免无限期等待。

    8.4.2 读写锁 (Read-Write Lock):QReadWriteLock, QReadLocker, QWriteLocker

    读写锁 (Read-Write Lock),也称为共享互斥锁 (Shared-Exclusive Lock),是一种比互斥锁更高级的同步机制。读写锁允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。读写锁适用于读多写少的场景,可以提高并发性能。

    读写锁的概念

    ⚝ 读写锁维护两种锁模式
    ▮▮▮▮⚝ 读模式 (Read Mode)共享模式 (Shared Mode): 允许多个线程同时持有读锁。
    ▮▮▮▮⚝ 写模式 (Write Mode)独占模式 (Exclusive Mode): 只允许一个线程持有写锁。
    读取规则
    ▮▮▮▮⚝ 当没有线程持有写锁时,多个线程可以同时获取读锁,并发读取共享资源。
    ▮▮▮▮⚝ 当有线程持有写锁时,其他任何线程(包括读线程和写线程)都不能获取锁,必须等待写锁释放。
    写入规则
    ▮▮▮▮⚝ 当没有线程持有任何锁(读锁或写锁)时,一个线程可以获取写锁,独占访问共享资源进行写入操作。
    ▮▮▮▮⚝ 当有线程持有读锁或写锁时,写线程不能获取写锁,必须等待所有读锁和写锁释放。
    优先级: 读写锁通常实现写优先策略,即当读线程和写线程同时请求锁时,优先满足写线程的请求,避免写线程饥饿 (Starvation)。

    Qt 读写锁类

    Qt 提供了 QReadWriteLock, QReadLocker, QWriteLocker 类来实现读写锁。

    QReadWriteLock: 读写锁类,提供了读锁和写锁的获取和释放操作。
    QReadLocker: 读锁包装类,RAII 风格,用于自动管理读锁的生命周期。
    QWriteLocker: 写锁包装类,RAII 风格,用于自动管理写锁的生命周期。

    使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QReadWriteLock>
    2 #include <QReadLocker>
    3 #include <QWriteLocker>
    4 #include <QDebug>
    5 #include <QThread>
    6
    7 QReadWriteLock dataLock; // 定义读写锁
    8 QList<int> dataList; // 共享数据列表
    9
    10 void readData() {
    11 QReadLocker readLocker(&dataLock); // 获取读锁
    12 qDebug() << "Read thread " << QThread::currentThreadId() << " reading data: " << dataList;
    13 QThread::msleep(100); // 模拟读取操作耗时
    14 }
    15
    16 void writeData(int value) {
    17 QWriteLocker writeLocker(&dataLock); // 获取写锁
    18 qDebug() << "Write thread " << QThread::currentThreadId() << " writing data: " << value;
    19 dataList.append(value); // 修改共享数据
    20 QThread::msleep(500); // 模拟写入操作耗时
    21 }
    22
    23 void testReadWriteLock() {
    24 QThread readThread1, readThread2, writeThread1, writeThread2;
    25
    26 QObject::connect(&readThread1, &QThread::started, [](){ readData(); });
    27 QObject::connect(&readThread2, &QThread::started, [](){ readData(); });
    28 QObject::connect(&writeThread1, &QThread::started, [](){ writeData(10); });
    29 QObject::connect(&writeThread2, &QThread::started, [](){ writeData(20); });
    30
    31 readThread1.start();
    32 readThread2.start();
    33 writeThread1.start();
    34 writeThread2.start();
    35
    36 readThread1.wait();
    37 readThread2.wait();
    38 writeThread1.wait();
    39 writeThread2.wait();
    40
    41 qDebug() << "Final data list: " << dataList; // 输出最终的数据列表
    42 }

    QReadWriteLock dataLock: 定义一个读写锁对象 dataLock,用于保护共享数据列表 dataList
    QReadLocker readLocker(&dataLock): 在 readData() 函数中,创建 QReadLocker 对象 readLocker,获取读锁。多个读线程可以同时获取读锁,并发读取 dataList
    QWriteLocker writeLocker(&dataLock): 在 writeData() 函数中,创建 QWriteLocker 对象 writeLocker,获取写锁。同一时刻只允许一个写线程获取写锁,独占修改 dataList

    适用场景

    读写锁适用于读多写少的场景。例如:

    缓存 (Cache): 多个线程可以并发读取缓存数据,但只有少数线程需要更新缓存数据。
    配置信息 (Configuration): 多个线程可以并发读取配置信息,但只有管理员线程需要修改配置信息。
    大型只读数据结构: 多个线程可以并发读取大型只读数据结构,例如:字典、索引等。

    写多读少读写频率相近的场景下,读写锁的性能可能不如互斥锁,因为读写锁的实现通常比互斥锁更复杂,开销更大。需要根据实际场景选择合适的同步机制。

    8.4.3 条件变量 (Condition Variable):QWaitCondition

    条件变量 (Condition Variable) 是一种更高级的线程同步机制,用于实现线程间的条件同步等待/唤醒操作。条件变量通常与互斥锁 (Mutex) 结合使用。

    条件变量的概念

    ⚝ 条件变量允许线程等待某个条件成立。当条件不成立时,线程会被阻塞并释放互斥锁。
    ⚝ 当其他线程改变了条件,并发出信号 (Signal) 通知条件变量时,等待条件变量的线程会被唤醒 (Wake Up),重新尝试获取互斥锁并检查条件是否成立。
    等待/唤醒机制: 条件变量实现了线程间的等待/唤醒机制,可以更有效地协调线程的执行顺序,避免忙等待 (Busy Waiting),提高系统资源利用率。

    QWaitCondition

    Qt 提供了 QWaitCondition 类来实现条件变量。 QWaitCondition 提供了以下主要方法:

    QWaitCondition::wait(QMutex *mutex, unsigned long time = ULONG_MAX): 使当前线程等待条件变量被唤醒。 wait() 函数必须与一个互斥锁 mutex 关联使用。
    ▮▮▮▮⚝ wait() 函数会原子地 (Atomically) 释放互斥锁 mutex,并阻塞当前线程。
    ▮▮▮▮⚝ 当条件变量被唤醒等待超时时, wait() 函数会重新获取互斥锁 mutex,并返回。
    ▮▮▮▮⚝ time 参数指定等待超时时间,单位是毫秒。默认值为 ULONG_MAX,表示无限期等待。
    QWaitCondition::wakeOne()唤醒一个等待条件变量的线程。如果有多个线程等待条件变量,则只唤醒其中一个线程。
    QWaitCondition::wakeAll()唤醒所有等待条件变量的线程。

    使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QMutex>
    2 #include <QWaitCondition>
    3 #include <QThread>
    4 #include <QDebug>
    5
    6 QMutex dataMutex; // 互斥锁,保护共享数据
    7 QWaitCondition dataReadyCondition; // 条件变量,用于线程同步
    8 bool dataReady = false; // 共享条件变量
    9
    10 void consumerThread() {
    11 QMutexLocker locker(&dataMutex); // 获取互斥锁
    12 qDebug() << "Consumer thread waiting for data...";
    13 while (!dataReady) {
    14 dataReadyCondition.wait(&dataMutex); // 等待条件变量被唤醒,并释放互斥锁
    15 }
    16 qDebug() << "Consumer thread received data, processing...";
    17 // ... 处理数据 ...
    18 }
    19
    20 void producerThread() {
    21 QMutexLocker locker(&dataMutex); // 获取互斥锁
    22 qDebug() << "Producer thread producing data...";
    23 QThread::sleep(2); // 模拟数据生产耗时
    24 dataReady = true; // 设置条件为真
    25 dataReadyCondition.wakeOne(); // 唤醒一个等待条件变量的线程
    26 qDebug() << "Producer thread data ready signal sent.";
    27 }
    28
    29 void testConditionVariable() {
    30 QThread consumer, producer;
    31
    32 QObject::connect(&consumer, &QThread::started, [](){ consumerThread(); });
    33 QObject::connect(&producer, &QThread::started, [](){ producerThread(); });
    34
    35 consumer.start();
    36 producer.start();
    37
    38 consumer.wait();
    39 producer.wait();
    40
    41 qDebug() << "Condition variable test finished.";
    42 }

    QMutex dataMutexQWaitCondition dataReadyCondition: 定义互斥锁 dataMutex 和条件变量 dataReadyCondition。条件变量 dataReadyCondition 用于在 consumerThread()producerThread() 之间进行同步。共享变量 dataReady 作为条件变量的条件。
    consumerThread(): 消费者线程。首先获取互斥锁 dataMutex,然后在一个循环中检查条件 dataReady 是否为 true。如果条件不成立,则调用 dataReadyCondition.wait(&dataMutex) 等待条件变量被唤醒。 wait() 函数会释放互斥锁,并阻塞线程。当条件变量被唤醒时, wait() 函数会重新获取互斥锁,并继续执行循环体。
    producerThread(): 生产者线程。首先获取互斥锁 dataMutex,然后模拟数据生产耗时。之后,设置条件变量 dataReady = true,并调用 dataReadyCondition.wakeOne() 唤醒一个等待条件变量的线程(这里是消费者线程)。
    等待条件变量dataReadyCondition.wait(&dataMutex) 函数必须在互斥锁的保护下调用。这是为了避免虚假唤醒 (Spurious Wakeup)丢失唤醒 (Lost Wakeup) 问题。

    适用场景

    条件变量适用于实现生产者-消费者模式 (Producer-Consumer Pattern)事件通知线程同步等场景。例如:

    生产者-消费者模式: 一个或多个生产者线程生产数据,放入缓冲区;一个或多个消费者线程从缓冲区取出数据进行消费。可以使用条件变量来同步生产者线程和消费者线程,当缓冲区为空时,消费者线程等待;当缓冲区有数据时,生产者线程唤醒消费者线程。
    事件通知: 一个线程等待某个事件发生,当事件发生时,另一个线程通知等待线程。可以使用条件变量来实现事件通知机制。

    8.4.4 原子操作 (Atomic Operations):QAtomicInt, QAtomicPointer

    原子操作 (Atomic Operations) 是一种不可中断的操作。原子操作要么完全执行,要么完全不执行,不会被其他线程的执行所中断。原子操作可以用于实现轻量级的线程同步,避免使用互斥锁带来的开销。

    原子操作的概念

    原子性 (Atomicity): 原子操作具有原子性,即操作是不可分割的,要么全部完成,要么全部不完成。在原子操作执行过程中,不会发生线程切换,也不会被其他线程中断。
    硬件支持: 原子操作通常由硬件指令直接支持,例如: CPU 提供的原子指令。因此,原子操作的执行效率非常高,开销很小。
    适用场景: 原子操作适用于对简单数据类型(例如:整数、指针等)进行简单操作(例如:自增、自减、比较交换等)的线程同步。

    Qt 原子操作类

    Qt 提供了 QAtomicInt, QAtomicPointer 等原子操作类,用于进行原子整数和原子指针操作。

    QAtomicInt: 原子整数类,提供了原子整数的各种操作,例如:自增、自减、加法、减法、位运算、比较交换等。
    QAtomicPointer<T>: 原子指针类,提供了原子指针的加载、存储、比较交换等操作。

    使用示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QAtomicInt>
    2 #include <QThread>
    3 #include <QDebug>
    4
    5 QAtomicInt atomicCounter(0); // 原子整数计数器
    6
    7 void incrementAtomicCounter() {
    8 for (int i = 0; i < 10000; ++i) {
    9 atomicCounter.fetchAndAddOrdered(1); // 原子自增操作
    10 }
    11 }
    12
    13 void testAtomicOperations() {
    14 QThread thread1, thread2;
    15
    16 QObject::connect(&thread1, &QThread::started, [](){ incrementAtomicCounter(); });
    17 QObject::connect(&thread2, &QThread::started, [](){ incrementAtomicCounter(); });
    18
    19 thread1.start();
    20 thread2.start();
    21
    22 thread1.wait();
    23 thread2.wait();
    24
    25 qDebug() << "Atomic counter value: " << atomicCounter.load(); // 原子加载计数器值
    26 }

    QAtomicInt atomicCounter(0): 定义一个原子整数对象 atomicCounter,初始值为 0。
    atomicCounter.fetchAndAddOrdered(1): 使用 fetchAndAddOrdered(1) 函数进行原子自增操作。 fetchAndAddOrdered(1) 函数会将 atomicCounter 的值原子地加 1,并返回操作前的值。
    atomicCounter.load(): 使用 load() 函数原子地加载 atomicCounter 的当前值。

    原子操作的优势

    性能高: 原子操作由硬件指令直接支持,执行效率非常高,开销很小。
    无锁 (Lock-Free): 原子操作不需要使用互斥锁等锁机制,避免了锁竞争和死锁的风险。

    原子操作的局限性

    操作简单: 原子操作只能用于对简单数据类型进行简单操作,无法实现复杂的同步逻辑。
    适用范围有限: 原子操作的适用范围有限,只适用于简单的计数器、标志位等场景。对于更复杂的共享数据和同步需求,仍然需要使用互斥锁、读写锁、条件变量等更高级的同步机制。

    选择线程同步机制

    互斥锁 (Mutex): 通用的线程同步机制,适用于保护临界区,实现互斥访问共享资源。
    读写锁 (Read-Write Lock): 适用于读多写少的场景,允许多个线程并发读取,提高并发性能。
    条件变量 (Condition Variable): 用于实现线程间的条件同步和等待/唤醒操作,适用于生产者-消费者模式、事件通知等场景。
    原子操作 (Atomic Operations): 适用于对简单数据类型进行简单操作的轻量级同步,性能高,无锁,但适用范围有限。

    在实际开发中,需要根据具体的同步需求和性能要求,选择合适的线程同步机制。可以结合使用多种同步机制,例如:使用互斥锁保护临界区,使用条件变量实现线程间的同步,使用原子操作进行轻量级计数等。

    9. 网络编程:Qt 网络模块应用

    章节概要

    本章将深入探讨 Qt 框架在网络编程方面的应用,重点介绍 Qt 网络模块 (Qt Network Module)。我们将系统地讲解如何利用 Qt 提供的强大工具和类库,实现各种网络通信功能,包括 TCP (Transmission Control Protocol) 和 UDP (User Datagram Protocol) 编程,以及如何处理 HTTP (Hypertext Transfer Protocol) 请求和响应,并涉及 SSL/TLS (Secure Sockets Layer/Transport Layer Security) 安全协议的应用。通过本章的学习,读者将能够掌握使用 Qt 进行高效、可靠和安全的网络应用程序开发的关键技术。

    9.1 Qt 网络模块 (Qt Network Module) 概述

    Qt 网络模块 (Qt Network Module) 是 Qt 框架中用于网络编程的核心模块,它提供了一系列类和函数,使得开发者能够轻松构建各种类型的网络应用程序。无论是客户端应用还是服务器端应用,无论是基于 TCP 的可靠连接还是基于 UDP 的快速数据报传输,Qt 网络模块都能提供强大的支持。同时,Qt 网络模块还支持现代网络应用中常用的 HTTP 协议,并集成了 SSL/TLS 协议,保障数据传输的安全性。

    9.1.1 Qt 网络模块的功能:TCP, UDP, HTTP, SSL/TLS

    Qt 网络模块提供了对多种网络协议的支持,涵盖了网络编程中常用的协议类型,主要功能包括:

    TCP (传输控制协议)
    ▮▮▮▮TCP 是一种面向连接的、可靠的、基于字节流的传输层通信协议。Qt 提供了 QTcpSocketQTcpServer 类,用于实现 TCP 客户端和服务器端的编程。通过 TCP 协议,可以建立可靠的网络连接,保证数据的顺序性和完整性,适用于对数据传输可靠性要求较高的应用,例如文件传输、远程控制等。

    UDP (用户数据报协议)
    ▮▮▮▮UDP 是一种无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。Qt 提供了 QUdpSocket 类,用于进行 UDP 编程。UDP 协议具有传输速度快、开销小等特点,适用于对数据传输实时性要求较高,但可以容忍少量数据丢失的应用,例如在线视频、网络游戏等。

    HTTP (超文本传输协议)
    ▮▮▮▮HTTP 是一种用于分布式、协作式和超媒体信息系统的应用层协议,是万维网数据通信的基础。Qt 提供了 QNetworkAccessManager 类,用于发送 HTTP 请求和处理 HTTP 响应。通过 QNetworkAccessManager,可以方便地实现 Web 客户端的功能,例如访问 Web API (应用程序编程接口)、下载网页资源等。

    SSL/TLS (安全套接层/传输层安全协议)
    ▮▮▮▮SSL/TLS 协议族用于在网络通信中提供安全性和数据完整性。Qt 网络模块内置了对 SSL/TLS 的支持,可以方便地创建安全的网络连接,保护数据在传输过程中的机密性和完整性。Qt 使用 OpenSSL 库来提供 SSL/TLS 功能,支持 HTTPS (HTTP Secure) 等安全协议。

    除了以上主要协议,Qt 网络模块还提供了其他网络相关的功能,例如:

    域名解析 (Domain Name Resolution):通过 QHostInfo 类,可以进行主机名到 IP (Internet Protocol) 地址的解析,以及反向解析。
    网络接口管理 (Network Interface Management):通过 QNetworkInterface 类,可以获取本地网络接口的信息,例如 IP 地址、MAC (Media Access Control) 地址等。
    代理 (Proxy) 支持:Qt 网络模块支持 HTTP、SOCKS (Socket Secure) 等代理协议,可以配置应用程序通过代理服务器进行网络访问。
    本地主机发现 (Local Host Discovery):通过 QTcpServerQUdpSocket 的广播功能,可以实现简单的本地主机发现机制。

    9.1.2 常用网络类:QTcpSocket, QUdpSocket, QTcpServer, QNetworkAccessManager

    Qt 网络模块提供了一系列核心类,用于实现不同类型的网络通信。以下是本章将重点介绍的几个常用网络类及其用途:

    QTcpSocket
    ▮▮▮▮QTcpSocket 类用于实现 TCP 客户端套接字 (Socket)。通过 QTcpSocket,可以连接到 TCP 服务器,并进行数据的发送和接收。QTcpSocket 提供了丰富的信号 (Signal) 和槽 (Slot),方便进行异步的 TCP 通信。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTcpSocket *tcpSocket = new QTcpSocket(this);
    2 tcpSocket->connectToHost("www.example.com", 80); // 连接到主机和端口
    3 if(tcpSocket->waitForConnected(1000)) { // 等待连接建立,超时时间 1 秒
    4 qDebug() << "Connected to server!";
    5 } else {
    6 qDebug() << "Connection failed: " << tcpSocket->errorString();
    7 }

    QUdpSocket
    ▮▮▮▮QUdpSocket 类用于实现 UDP 套接字 (Socket)。通过 QUdpSocket,可以发送和接收 UDP 数据报。QUdpSocket 也提供了信号和槽,用于处理异步的 UDP 数据接收事件。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QUdpSocket *udpSocket = new QUdpSocket(this);
    2 QByteArray datagram = "Hello, UDP!";
    3 udpSocket->writeDatagram(datagram, QHostAddress::Broadcast, 45454); // 发送广播数据报

    QTcpServer
    ▮▮▮▮QTcpServer 类用于实现 TCP 服务器端,监听指定端口,等待客户端的连接。当有客户端连接请求到达时,QTcpServer 会发出 newConnection() 信号,通过连接该信号的槽函数,可以获取到与客户端通信的 QTcpSocket 对象。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTcpServer *tcpServer = new QTcpServer(this);
    2 if (tcpServer->listen(QHostAddress::Any, 12345)) { // 监听所有地址的 12345 端口
    3 qDebug() << "Server started, listening on port 12345";
    4 connect(tcpServer, &QTcpServer::newConnection, this, [=](){
    5 QTcpSocket *clientConnection = tcpServer->nextPendingConnection(); // 获取客户端连接
    6 qDebug() << "Client connected: " << clientConnection->peerAddress().toString();
    7 clientConnection->disconnectFromHost(); // 断开连接 (示例,实际应用中需要处理数据交互)
    8 });
    9 } else {
    10 qDebug() << "Server could not start: " << tcpServer->errorString();
    11 }

    QNetworkAccessManager
    ▮▮▮▮QNetworkAccessManager 类用于发送网络请求,例如 HTTP 请求。它是一个高级类,可以方便地处理 HTTP 的各种方法(GET, POST, PUT, DELETE 等),并管理网络请求和响应。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QNetworkAccessManager *manager = new QNetworkAccessManager(this);
    2 QNetworkRequest request(QUrl("http://www.example.com")); // 创建 HTTP 请求
    3 QNetworkReply *reply = manager->get(request); // 发送 GET 请求
    4 connect(reply, &QNetworkReply::finished, this, [=](){
    5 if (reply->error() == QNetworkReply::NoError) {
    6 QByteArray responseData = reply->readAll();
    7 qDebug() << "HTTP Response: " << responseData;
    8 } else {
    9 qDebug() << "HTTP Error: " << reply->errorString();
    10 }
    11 reply->deleteLater(); // 释放 reply 对象
    12 });

    在接下来的章节中,我们将详细介绍如何使用这些类进行 TCP、UDP 和 HTTP 编程,并通过实例演示 Qt 网络模块在实际应用中的强大功能。

    9.2 TCP 编程:客户端与服务器

    TCP (Transmission Control Protocol) 编程是网络编程中非常重要的一部分。TCP 协议提供面向连接的、可靠的数据传输服务,适用于需要保证数据完整性和顺序性的应用场景。Qt 提供了 QTcpSocket 类用于实现 TCP 客户端,QTcpServer 类用于实现 TCP 服务器端。

    9.2.1 TCP 客户端 (Client) 的创建:QTcpSocket

    创建 TCP 客户端主要使用 QTcpSocket 类。以下是创建 TCP 客户端并连接到服务器的基本步骤:

    创建 QTcpSocket 对象
    ▮▮▮▮首先,需要创建一个 QTcpSocket 类的实例。通常在堆上动态分配,以便在需要时使用,并在适当的时候手动释放内存。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTcpSocket *tcpClient = new QTcpSocket(this);

    连接到服务器
    ▮▮▮▮使用 connectToHost() 函数连接到指定的服务器。connectToHost() 函数接受服务器的主机名或 IP 地址以及端口号作为参数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QString serverAddress = "127.0.0.1"; // 服务器 IP 地址,例如本地回环地址
    2 quint16 serverPort = 12345; // 服务器端口号
    3 tcpClient->connectToHost(serverAddress, serverPort);

    等待连接建立 (可选)
    ▮▮▮▮connectToHost() 函数是异步操作,连接建立可能需要一些时间。可以使用 waitForConnected() 函数等待连接建立完成,可以设置超时时间。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 if (tcpClient->waitForConnected(1000)) { // 等待 1 秒超时
    2 qDebug() << "Connected to server: " << serverAddress << ":" << serverPort;
    3 } else {
    4 qDebug() << "Connection failed: " << tcpClient->errorString();
    5 // 处理连接失败的情况,例如提示用户或重试连接
    6 }

    处理连接状态信号
    ▮▮▮▮QTcpSocket 类提供了一些信号用于指示连接状态,例如 connected() 信号在成功连接到服务器后发出,disconnected() 信号在连接断开后发出,error() 信号在发生错误时发出。可以连接这些信号到相应的槽函数,以便在连接状态发生变化时进行处理。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(tcpClient, &QTcpSocket::connected, this, [=](){
    2 qDebug() << "TCP connected signal received.";
    3 // 连接建立后的处理,例如发送数据
    4 });
    5
    6 connect(tcpClient, &QTcpSocket::disconnected, this, [=](){
    7 qDebug() << "TCP disconnected signal received.";
    8 // 连接断开后的处理,例如清理资源或尝试重连
    9 tcpClient->deleteLater(); // 释放 tcpClient 对象
    10 });
    11
    12 connect(tcpClient, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this,
    13 [=](QAbstractSocket::SocketError socketError){
    14 qDebug() << "TCP error signal received: " << tcpClient->errorString();
    15 // 处理错误,例如提示用户错误信息
    16 });

    一个完整的 TCP 客户端连接示例代码如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QTcpSocket>
    2 #include <QDebug>
    3
    4 void TcpClientExample() {
    5 QTcpSocket *tcpClient = new QTcpSocket();
    6 QString serverAddress = "127.0.0.1";
    7 quint16 serverPort = 12345;
    8
    9 tcpClient->connectToHost(serverAddress, serverPort);
    10
    11 QObject::connect(tcpClient, &QTcpSocket::connected, [](){
    12 qDebug() << "TCP connected!";
    13 // 连接建立后的操作
    14 });
    15
    16 QObject::connect(tcpClient, &QTcpSocket::disconnected, [](){
    17 qDebug() << "TCP disconnected!";
    18 // 连接断开后的操作
    19 });
    20
    21 QObject::connect(tcpClient, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error),
    22 [](QAbstractSocket::SocketError socketError){
    23 qDebug() << "TCP error: " << socketError;
    24 // 错误处理
    25 });
    26
    27 if (!tcpClient->waitForConnected(3000)) { // 等待连接超时
    28 qDebug() << "Connection timeout: " << tcpClient->errorString();
    29 tcpClient->deleteLater(); // 清理资源
    30 }
    31 // 后续的数据发送和接收操作将在连接建立后进行 (示例中未展示)
    32 }

    9.2.2 TCP 服务器 (Server) 的创建:QTcpServer, QTcpSocket

    创建 TCP 服务器端主要使用 QTcpServerQTcpSocket 类。QTcpServer 负责监听端口,接收客户端连接请求,QTcpSocket 用于与已连接的客户端进行数据交互。以下是创建 TCP 服务器的基本步骤:

    创建 QTcpServer 对象
    ▮▮▮▮首先,创建一个 QTcpServer 类的实例。同样,通常在堆上动态分配。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTcpServer *tcpServer = new QTcpServer(this);

    监听端口
    ▮▮▮▮使用 listen() 函数开始监听指定的端口。listen() 函数接受监听的地址和端口号作为参数。可以使用 QHostAddress::Any 监听所有可用的网络接口地址。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 quint16 listenPort = 12345;
    2 if (tcpServer->listen(QHostAddress::Any, listenPort)) {
    3 qDebug() << "TCP server started, listening on port: " << listenPort;
    4 } else {
    5 qDebug() << "Server could not start: " << tcpServer->errorString();
    6 // 处理服务器启动失败的情况,例如端口被占用
    7 }

    处理 newConnection() 信号
    ▮▮▮▮当有新的客户端连接请求到达时,QTcpServer 会发出 newConnection() 信号。需要连接该信号到一个槽函数,在槽函数中,使用 nextPendingConnection() 函数获取新连接的 QTcpSocket 对象。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(tcpServer, &QTcpServer::newConnection, this, [=](){
    2 QTcpSocket *clientSocket = tcpServer->nextPendingConnection(); // 获取客户端套接字
    3 if (clientSocket) {
    4 qDebug() << "Client connected from: " << clientSocket->peerAddress().toString() << ":" << clientSocket->peerPort();
    5 // 对 clientSocket 进行后续的数据接收和发送处理
    6 // 同样需要处理 clientSocket 的 disconnected 和 error 信号
    7 connect(clientSocket, &QTcpSocket::disconnected, this, [=](){
    8 qDebug() << "Client disconnected: " << clientSocket->peerAddress().toString() << ":" << clientSocket->peerPort();
    9 clientSocket->deleteLater(); // 释放 clientSocket 对象
    10 });
    11 connect(clientSocket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this,
    12 [=](QAbstractSocket::SocketError socketError){
    13 qDebug() << "Client socket error: " << clientSocket->errorString();
    14 clientSocket->deleteLater(); // 释放 clientSocket 对象
    15 });
    16 // ... 数据接收和发送处理 ...
    17 }
    18 });

    客户端套接字 (QTcpSocket) 的处理
    ▮▮▮▮在 newConnection() 信号的槽函数中获取到的 QTcpSocket 对象 clientSocket,代表了与新连接客户端进行通信的套接字。需要对 clientSocket 进行数据接收、发送和连接状态管理。和客户端类似,需要连接 disconnected()error() 信号,并在适当的时候释放 clientSocket 对象。

    一个简单的 TCP 服务器示例代码如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QTcpServer>
    2 #include <QTcpSocket>
    3 #include <QDebug>
    4
    5 class TcpServerExample : public QObject {
    6 Q_OBJECT
    7 public:
    8 TcpServerExample(QObject *parent = nullptr) : QObject(parent), tcpServer(new QTcpServer(this)) {
    9 quint16 listenPort = 12345;
    10 if (tcpServer->listen(QHostAddress::Any, listenPort)) {
    11 qDebug() << "TCP server started, listening on port: " << listenPort;
    12 connect(tcpServer, &QTcpServer::newConnection, this, &TcpServerExample::handleNewConnection);
    13 } else {
    14 qDebug() << "Server could not start: " << tcpServer->errorString();
    15 }
    16 }
    17
    18 private slots:
    19 void handleNewConnection() {
    20 QTcpSocket *clientSocket = tcpServer->nextPendingConnection();
    21 if (clientSocket) {
    22 qDebug() << "Client connected from: " << clientSocket->peerAddress().toString() << ":" << clientSocket->peerPort();
    23 connect(clientSocket, &QTcpSocket::disconnected, this, &TcpServerExample::handleClientDisconnection);
    24 connect(clientSocket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &TcpServerExample::handleClientError);
    25 // ... 数据接收和发送处理 ... (示例中未展示)
    26 }
    27 }
    28
    29 void handleClientDisconnection() {
    30 QTcpSocket *clientSocket = qobject_cast<QTcpSocket*>(sender());
    31 if (clientSocket) {
    32 qDebug() << "Client disconnected: " << clientSocket->peerAddress().toString() << ":" << clientSocket->peerPort();
    33 clientSocket->deleteLater();
    34 }
    35 }
    36
    37 void handleClientError(QAbstractSocket::SocketError socketError) {
    38 QTcpSocket *clientSocket = qobject_cast<QTcpSocket*>(sender());
    39 if (clientSocket) {
    40 qDebug() << "Client socket error: " << clientSocket->errorString();
    41 clientSocket->deleteLater();
    42 }
    43 }
    44
    45 private:
    46 QTcpServer *tcpServer;
    47 };
    48
    49 #include "tcpserverexample.moc" // 需要 moc 生成

    9.2.3 TCP 数据传输:发送与接收数据

    在 TCP 连接建立之后,客户端和服务器端可以进行数据的发送和接收。QTcpSocket 提供了 write() 函数用于发送数据,以及 readyRead() 信号用于指示有新数据到达。

    发送数据
    ▮▮▮▮使用 write() 函数向 TCP 连接发送数据。write() 函数接受 QByteArrayconst char* 类型的数据作为参数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTcpSocket *clientSocket; // 已连接的客户端套接字
    2 QByteArray dataToSend = "Hello from client!";
    3 qint64 bytesWritten = clientSocket->write(dataToSend);
    4 if (bytesWritten == -1) {
    5 qDebug() << "Error sending data: " << clientSocket->errorString();
    6 } else {
    7 qDebug() << "Data sent: " << bytesWritten << " bytes";
    8 }

    接收数据
    ▮▮▮▮当有新的数据到达时,QTcpSocket 会发出 readyRead() 信号。需要连接该信号到一个槽函数,在槽函数中使用 readAll()read() 函数读取接收到的数据。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTcpSocket *clientSocket; // 已连接的客户端套接字
    2 connect(clientSocket, &QTcpSocket::readyRead, this, [=](){
    3 QByteArray receivedData = clientSocket->readAll();
    4 qDebug() << "Received data from client: " << receivedData;
    5 // 处理接收到的数据,例如解析协议、更新界面等
    6 });

    数据缓冲区
    ▮▮▮▮TCP 协议是基于字节流的,数据可能被分段传输,因此接收端可能需要处理数据缓冲区。当 readyRead() 信号发出时,并不意味着一次性接收到了完整的数据包,可能需要多次读取才能接收到完整的数据。在实际应用中,通常需要定义数据包的协议格式(例如,固定长度头部 + 数据体,或者使用分隔符),以便正确解析接收到的数据。

    一个简单的数据发送和接收的示例代码片段:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // ... (在 TCP 客户端或服务器端的连接建立后) ...
    2
    3 // 发送数据 (客户端或服务器端)
    4 QByteArray dataToSend = "Hello, TCP data!";
    5 clientSocket->write(dataToSend);
    6
    7 // 接收数据 (客户端或服务器端)
    8 connect(clientSocket, &QTcpSocket::readyRead, this, [=](){
    9 while(clientSocket->bytesAvailable() > 0) { // 循环读取所有可用的数据
    10 QByteArray receivedData = clientSocket->readAll(); // 读取所有可用数据
    11 qDebug() << "Received data: " << receivedData;
    12 // 处理接收到的数据
    13 }
    14 });

    9.2.4 异步 TCP 通信:信号与槽

    Qt 的信号与槽机制在 TCP 通信中扮演着非常重要的角色,它使得异步 TCP 通信的实现变得简洁而高效。QTcpSocket 类提供了丰富的信号,用于通知应用程序网络事件的发生,例如连接建立、连接断开、有数据可读、错误发生等。通过连接这些信号到自定义的槽函数,可以实现异步事件驱动的网络编程模式。

    事件驱动模型
    ▮▮▮▮Qt 的 TCP 通信是基于事件驱动模型的。应用程序不需要轮询 (Polling) 套接字状态,而是等待 QTcpSocket 发出相应的信号,然后响应这些信号进行处理。这种模型提高了程序的效率和响应性,避免了不必要的 CPU (中央处理器) 资源消耗。

    常用信号与槽的连接
    ▮▮▮▮在前面的章节中,我们已经看到了如何连接 connected(), disconnected(), error(), readyRead() 等信号。这些信号覆盖了 TCP 通信中常用的事件类型。通过连接这些信号到槽函数,可以实现对 TCP 连接状态、数据接收和错误处理的异步响应。

    异步操作的优势
    ▮▮▮▮异步 TCP 通信避免了阻塞 (Blocking) 操作,使得 GUI (图形用户界面) 应用程序在进行网络通信时不会卡顿。例如,在接收大量数据时,或者在网络延迟较高的情况下,如果使用同步 (Blocking) 的 read() 函数,可能会导致程序界面卡死,影响用户体验。而使用异步的 readyRead() 信号,数据接收过程在后台进行,不会阻塞主线程,保证了 GUI 的流畅性。

    信号与槽的灵活性
    ▮▮▮▮Qt 的信号与槽机制非常灵活,可以实现一对一、一对多、多对一的信号槽连接。在 TCP 通信中,可以根据需要连接不同的信号到不同的槽函数,实现复杂的事件处理逻辑。例如,可以将 readyRead() 信号连接到多个槽函数,分别处理不同类型的数据,或者将多个 QTcpSocket 对象的信号连接到同一个槽函数,统一处理多个连接的事件。

    一个使用信号与槽进行异步 TCP 通信的示例框架:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyTcpClient : public QObject {
    2 Q_OBJECT
    3 public:
    4 MyTcpClient(QObject *parent = nullptr) : QObject(parent), tcpSocket(new QTcpSocket(this)) {
    5 connect(tcpSocket, &QTcpSocket::connected, this, &MyTcpClient::onConnected);
    6 connect(tcpSocket, &QTcpSocket::disconnected, this, &MyTcpClient::onDisconnected);
    7 connect(tcpSocket, &QTcpSocket::readyRead, this, &MyTcpClient::onReadyRead);
    8 connect(tcpSocket, QOverload<QAbstractSocket::SocketError>::of(&QTcpSocket::error), this, &MyTcpClient::onError);
    9 }
    10
    11 void connectToServer(const QString &host, quint16 port) {
    12 tcpSocket->connectToHost(host, port);
    13 }
    14
    15 void sendData(const QByteArray &data) {
    16 tcpSocket->write(data);
    17 }
    18
    19 private slots:
    20 void onConnected() {
    21 qDebug() << "Connected to server!";
    22 // 连接成功后的处理
    23 }
    24
    25 void onDisconnected() {
    26 qDebug() << "Disconnected from server!";
    27 tcpSocket->deleteLater(); // 清理 socket 对象
    28 // 断开连接后的处理
    29 }
    30
    31 void onReadyRead() {
    32 QByteArray data = tcpSocket->readAll();
    33 qDebug() << "Received data: " << data;
    34 // 处理接收到的数据
    35 }
    36
    37 void onError(QAbstractSocket::SocketError error) {
    38 qDebug() << "Socket error: " << tcpSocket->errorString();
    39 tcpSocket->deleteLater(); // 清理 socket 对象
    40 // 错误处理
    41 }
    42
    43 private:
    44 QTcpSocket *tcpSocket;
    45 };
    46
    47 #include "mytcpclient.moc" // 需要 moc 生成

    9.3 UDP 编程:数据报通信

    UDP (User Datagram Protocol) 是一种无连接的传输层协议,提供快速、简单的数据报传输服务。与 TCP 不同,UDP 不保证数据的可靠性和顺序性,但具有传输速度快、开销小等优点,适用于对实时性要求较高,可以容忍少量数据丢失的应用场景,例如在线视频、网络游戏、实时监控等。Qt 提供了 QUdpSocket 类用于进行 UDP 编程。

    9.3.1 UDP 套接字 (Socket) 的创建:QUdpSocket

    创建 UDP 套接字主要使用 QUdpSocket 类。以下是创建 UDP 套接字的基本步骤:

    创建 QUdpSocket 对象
    ▮▮▮▮首先,需要创建一个 QUdpSocket 类的实例。同样,通常在堆上动态分配。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QUdpSocket *udpSocket = new QUdpSocket(this);

    绑定地址和端口 (可选)
    ▮▮▮▮UDP 套接字可以选择绑定到特定的本地地址和端口。如果不绑定,系统会自动分配一个可用的端口。绑定地址和端口通常用于接收数据报,特别是当需要接收广播或组播数据时。使用 bind() 函数进行绑定。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 quint16 bindPort = 45454;
    2 if (udpSocket->bind(bindPort)) { // 绑定到指定端口,监听所有地址
    3 qDebug() << "UDP socket binded to port: " << bindPort;
    4 } else {
    5 qDebug() << "UDP socket bind failed: " << udpSocket->errorString();
    6 // 处理绑定失败的情况,例如端口被占用
    7 }
    8 // 也可以绑定到特定地址和端口:
    9 // if (udpSocket->bind(QHostAddress::AnyIPv4, bindPort)) { ... }

    处理 readyRead() 信号
    ▮▮▮▮当有新的 UDP 数据报到达时,QUdpSocket 会发出 readyRead() 信号。需要连接该信号到一个槽函数,在槽函数中使用 readDatagram() 函数读取接收到的数据报。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(udpSocket, &QUdpSocket::readyRead, this, [=](){
    2 while (udpSocket->hasPendingDatagrams()) { // 循环处理所有待处理的数据报
    3 QByteArray datagram;
    4 datagram.resize(udpSocket->pendingDatagramSize()); // 调整 datagram 大小以容纳数据报
    5 QHostAddress senderAddress;
    6 quint16 senderPort;
    7 udpSocket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort); // 读取数据报
    8 qDebug() << "Received datagram from: " << senderAddress.toString() << ":" << senderPort << ", data: " << datagram;
    9 // 处理接收到的数据报,例如解析协议、更新界面等
    10 }
    11 });

    一个简单的 UDP 套接字创建和数据接收示例代码如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QUdpSocket>
    2 #include <QDebug>
    3 #include <QHostAddress>
    4
    5 void UdpSocketExample() {
    6 QUdpSocket *udpReceiver = new QUdpSocket();
    7 quint16 bindPort = 45454;
    8
    9 if (udpReceiver->bind(bindPort)) {
    10 qDebug() << "UDP receiver started, listening on port: " << bindPort;
    11 } else {
    12 qDebug() << "UDP bind failed: " << udpReceiver->errorString();
    13 udpReceiver->deleteLater();
    14 return;
    15 }
    16
    17 QObject::connect(udpReceiver, &QUdpSocket::readyRead, [=](){
    18 while (udpReceiver->hasPendingDatagrams()) {
    19 QByteArray datagram;
    20 datagram.resize(udpReceiver->pendingDatagramSize());
    21 QHostAddress senderAddress;
    22 quint16 senderPort;
    23 udpReceiver->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort);
    24 qDebug() << "UDP received from: " << senderAddress.toString() << ":" << senderPort << ", data: " << datagram;
    25 // 处理接收到的 UDP 数据报
    26 }
    27 });
    28
    29 // 数据发送将在后续章节介绍
    30 }

    9.3.2 UDP 数据传输:发送与接收数据报

    UDP 数据传输是基于数据报 (Datagram) 的,发送端将数据封装成数据报,指定目标地址和端口,然后发送出去。接收端接收到数据报后,可以从中提取数据和发送端信息。QUdpSocket 提供了 writeDatagram() 函数用于发送数据报,readDatagram() 函数用于接收数据报。

    发送数据报
    ▮▮▮▮使用 writeDatagram() 函数发送 UDP 数据报。writeDatagram() 函数有多个重载版本,常用的版本接受数据、目标主机地址和目标端口号作为参数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QUdpSocket *udpSocket; // UDP 套接字对象
    2 QByteArray dataToSend = "Hello, UDP datagram!";
    3 QHostAddress targetAddress = QHostAddress::Broadcast; // 广播地址
    4 quint16 targetPort = 45454;
    5
    6 qint64 bytesSent = udpSocket->writeDatagram(dataToSend, targetAddress, targetPort);
    7 if (bytesSent == -1) {
    8 qDebug() << "Error sending UDP datagram: " << udpSocket->errorString();
    9 } else {
    10 qDebug() << "UDP datagram sent: " << bytesSent << " bytes to " << targetAddress.toString() << ":" << targetPort;
    11 }

    接收数据报
    ▮▮▮▮使用 readDatagram() 函数接收 UDP 数据报。readDatagram() 函数需要预先分配一个缓冲区,用于存储接收到的数据。同时,可以获取发送端的地址和端口号。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QUdpSocket *udpSocket; // UDP 套接字对象
    2 QByteArray receivedDatagram;
    3 receivedDatagram.resize(udpSocket->pendingDatagramSize()); // 调整缓冲区大小
    4 QHostAddress senderAddress;
    5 quint16 senderPort;
    6
    7 qint64 bytesRead = udpSocket->readDatagram(receivedDatagram.data(), receivedDatagram.size(), &senderAddress, &senderPort);
    8 if (bytesRead == -1) {
    9 qDebug() << "Error receiving UDP datagram: " << udpSocket->errorString();
    10 } else {
    11 qDebug() << "UDP datagram received from " << senderAddress.toString() << ":" << senderPort << ", data: " << receivedDatagram;
    12 // 处理接收到的数据报
    13 }

    数据报大小限制
    ▮▮▮▮UDP 协议对数据报的大小有限制,通常受到网络 MTU (最大传输单元) 的限制。超过 MTU 的数据报可能会被分片,并在接收端重组,但 UDP 本身不保证数据报的完整性。因此,在 UDP 编程中,应尽量控制数据报的大小,避免超过网络 MTU,或者在应用层实现数据分片和重组机制。

    一个简单的 UDP 数据报发送和接收的示例代码片段:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // ... (UDP 套接字创建和绑定后) ...
    2
    3 // 发送 UDP 数据报
    4 QUdpSocket *udpSender = new QUdpSocket();
    5 QByteArray dataToSend = "Hello, UDP broadcast!";
    6 QHostAddress broadcastAddress = QHostAddress::Broadcast;
    7 quint16 targetPort = 45454;
    8 udpSender->writeDatagram(dataToSend, broadcastAddress, targetPort);
    9 qDebug() << "UDP broadcast datagram sent.";
    10 udpSender->deleteLater(); // 发送完即可释放 sender
    11
    12 // 接收 UDP 数据报 (在接收端 readyRead 信号的槽函数中)
    13 while (udpSocket->hasPendingDatagrams()) {
    14 QByteArray datagram;
    15 datagram.resize(udpSocket->pendingDatagramSize());
    16 QHostAddress senderAddress;
    17 quint16 senderPort;
    18 udpSocket->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort);
    19 qDebug() << "UDP received from: " << senderAddress.toString() << ":" << senderPort << ", data: " << datagram;
    20 // 处理接收到的 UDP 数据报
    21 }

    9.3.3 UDP 广播 (Broadcast) 与组播 (Multicast)

    UDP 广播 (Broadcast) 和组播 (Multicast) 是一种高效的数据分发方式,可以将数据发送给网络中的多个接收者,而无需为每个接收者单独发送一份数据。Qt 的 QUdpSocket 类支持 UDP 广播和组播。

    UDP 广播 (Broadcast)
    ▮▮▮▮UDP 广播将数据报发送给局域网 (LAN) 内的所有主机。广播地址通常是网络地址的最后一部分设置为全 1 的地址,例如 IPv4 的广播地址通常是 255.255.255.255 或网络号 + 255。Qt 提供了 QHostAddress::Broadcast 常量表示广播地址。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QUdpSocket *udpBroadcaster = new QUdpSocket();
    2 QByteArray broadcastMessage = "UDP broadcast message!";
    3 QHostAddress broadcastAddress = QHostAddress::Broadcast;
    4 quint16 broadcastPort = 45454;
    5
    6 udpBroadcaster->writeDatagram(broadcastMessage, broadcastAddress, broadcastPort);
    7 qDebug() << "UDP broadcast message sent.";
    8 udpBroadcaster->deleteLater();

    ▮▮▮▮注意:广播数据报只能在本地局域网内传播,无法跨越路由器 (Router) 传播到其他网络。广播可能会增加网络负载,应谨慎使用。

    UDP 组播 (Multicast)
    ▮▮▮▮UDP 组播将数据报发送给加入特定组播组的主机。组播地址是一组特殊的 IP 地址范围(IPv4 的组播地址范围是 224.0.0.0239.255.255.255)。只有加入该组播组的主机才能接收到组播数据报。Qt 提供了 joinMulticastGroup() 函数用于加入组播组,leaveMulticastGroup() 函数用于离开组播组。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QUdpSocket *udpMulticastReceiver = new QUdpSocket();
    2 QHostAddress multicastAddress("225.4.5.6"); // 组播地址
    3 quint16 multicastPort = 45454;
    4
    5 if (udpMulticastReceiver->bind(QHostAddress::AnyIPv4, multicastPort, QUdpSocket::ShareAddress)) { // 绑定端口,允许共享地址
    6 qDebug() << "UDP multicast receiver binded to port: " << multicastPort;
    7 if (udpMulticastReceiver->joinMulticastGroup(multicastAddress)) { // 加入组播组
    8 qDebug() << "Joined multicast group: " << multicastAddress.toString();
    9 } else {
    10 qDebug() << "Failed to join multicast group: " << udpMulticastReceiver->errorString();
    11 }
    12 } else {
    13 qDebug() << "UDP bind failed: " << udpMulticastReceiver->errorString();
    14 }
    15
    16 QObject::connect(udpMulticastReceiver, &QUdpSocket::readyRead, [=](){
    17 while (udpMulticastReceiver->hasPendingDatagrams()) {
    18 QByteArray datagram;
    19 datagram.resize(udpMulticastReceiver->pendingDatagramSize());
    20 QHostAddress senderAddress;
    21 quint16 senderPort;
    22 udpMulticastReceiver->readDatagram(datagram.data(), datagram.size(), &senderAddress, &senderPort);
    23 qDebug() << "UDP multicast received from: " << senderAddress.toString() << ":" << senderPort << ", data: " << datagram;
    24 // 处理接收到的组播数据报
    25 }
    26 });
    27
    28 // 发送组播数据报
    29 QUdpSocket *udpMulticastSender = new QUdpSocket();
    30 QByteArray multicastMessage = "UDP multicast message!";
    31 udpMulticastSender->writeDatagram(multicastMessage, multicastAddress, multicastPort);
    32 qDebug() << "UDP multicast message sent.";
    33 udpMulticastSender->deleteLater();

    ▮▮▮▮注意:组播比广播更高效,因为只有加入组播组的主机才会接收到数据报,减少了网络负载。组播可以跨越支持组播路由的路由器传播到其他网络。使用组播需要网络设备支持组播功能。

    9.4 HTTP 编程:QNetworkAccessManager

    HTTP (Hypertext Transfer Protocol) 是应用层协议,用于在 Web 浏览器和 Web 服务器之间传输数据。Qt 提供了 QNetworkAccessManager 类,用于发送 HTTP 请求和处理 HTTP 响应,方便实现 Web 客户端的功能。

    9.4.1 QNetworkAccessManager 的使用:发送 HTTP 请求

    QNetworkAccessManager 类是 Qt 中进行 HTTP 编程的核心类。使用 QNetworkAccessManager 发送 HTTP 请求的基本步骤如下:

    创建 QNetworkAccessManager 对象
    ▮▮▮▮首先,创建一个 QNetworkAccessManager 类的实例。通常在堆上动态分配。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QNetworkAccessManager *networkManager = new QNetworkAccessManager(this);

    创建 QNetworkRequest 对象
    ▮▮▮▮创建一个 QNetworkRequest 对象,用于封装 HTTP 请求的信息,例如 URL (统一资源定位符)、请求头 (Headers) 等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QUrl requestUrl("http://www.example.com/api/data"); // 请求 URL
    2 QNetworkRequest request(requestUrl);

    发送 HTTP 请求
    ▮▮▮▮使用 QNetworkAccessManagerget(), post(), put(), deleteResource() 等函数发送不同类型的 HTTP 请求。这些函数接受 QNetworkRequest 对象作为参数,并返回一个 QNetworkReply 对象,用于处理 HTTP 响应。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 发送 GET 请求
    2 QNetworkReply *reply = networkManager->get(request);
    3
    4 // 发送 POST 请求 (需要提供请求体数据)
    5 QByteArray postData = "key1=value1&key2=value2";
    6 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); // 设置 Content-Type
    7 QNetworkReply *reply = networkManager->post(request, postData);
    8
    9 // 发送 PUT 请求 (需要提供请求体数据)
    10 QByteArray putData = "{\"name\": \"new_resource\", \"value\": \"data\"}";
    11 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); // 设置 Content-Type
    12 QNetworkReply *reply = networkManager->put(request, putData);
    13
    14 // 发送 DELETE 请求
    15 QNetworkReply *reply = networkManager->deleteResource(request);

    处理 QNetworkReply 响应
    ▮▮▮▮QNetworkReply 对象用于处理 HTTP 响应。需要连接 finished() 信号到槽函数,在槽函数中检查响应状态、读取响应数据,并释放 QNetworkReply 对象。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(reply, &QNetworkReply::finished, this, [=](){
    2 if (reply->error() == QNetworkReply::NoError) {
    3 QByteArray responseData = reply->readAll(); // 读取响应体数据
    4 qDebug() << "HTTP Response status code: " << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    5 qDebug() << "HTTP Response data: " << responseData;
    6 // 处理响应数据
    7 } else {
    8 qDebug() << "HTTP Error: " << reply->errorString();
    9 qDebug() << "HTTP Error code: " << reply->error();
    10 // 处理错误
    11 }
    12 reply->deleteLater(); // 释放 reply 对象
    13 });

    一个简单的 HTTP GET 请求示例代码如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QNetworkAccessManager>
    2 #include <QNetworkRequest>
    3 #include <QNetworkReply>
    4 #include <QUrl>
    5 #include <QDebug>
    6
    7 void HttpRequestExample() {
    8 QNetworkAccessManager *manager = new QNetworkAccessManager();
    9 QUrl url("http://api.example.com/data"); // 替换为实际的 API URL
    10 QNetworkRequest request(url);
    11
    12 QNetworkReply *reply = manager->get(request);
    13
    14 QObject::connect(reply, &QNetworkReply::finished, [=](){
    15 if (reply->error() == QNetworkReply::NoError) {
    16 QByteArray responseData = reply->readAll();
    17 qDebug() << "HTTP GET response: " << responseData;
    18 // 处理响应数据
    19 } else {
    20 qDebug() << "HTTP GET error: " << reply->errorString();
    21 }
    22 reply->deleteLater(); // 释放 reply 对象
    23 manager->deleteLater(); // 释放 manager 对象 (可选,如果不再使用)
    24 });
    25 }

    9.4.2 HTTP 请求的配置:请求头 (Headers), 请求体 (Body)

    HTTP 请求可以通过请求头 (Headers) 和请求体 (Body) 传递额外的参数和数据。QNetworkRequest 类提供了设置请求头的方法,QNetworkAccessManagerpost(), put() 等函数接受请求体数据作为参数。

    设置请求头 (Headers)
    ▮▮▮▮使用 setHeader() 函数设置请求头。setHeader() 函数接受请求头名称和值作为参数。常用的请求头包括 Content-Type, Authorization, User-Agent 等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QNetworkRequest request(QUrl("http://www.example.com/api"));
    2 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); // 设置 Content-Type 为 JSON
    3 request.setHeader(QNetworkRequest::AuthorizationHeader, "Bearer your_access_token"); // 设置 Authorization 头
    4 request.setHeader(QNetworkRequest::UserAgentHeader, "MyQtApp/1.0"); // 设置 User-Agent

    设置请求体 (Body) 数据
    ▮▮▮▮对于 POST, PUT 等请求,需要提供请求体数据。请求体数据通常是 QByteArray 类型,可以是文本数据、JSON 数据、二进制数据等。请求体数据的格式需要通过 Content-Type 请求头指定。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 发送 JSON 数据作为请求体
    2 QJsonObject jsonData;
    3 jsonData["name"] = "John Doe";
    4 jsonData["email"] = "john.doe@example.com";
    5 QJsonDocument jsonDoc(jsonData);
    6 QByteArray requestBody = jsonDoc.toJson();
    7
    8 QNetworkRequest request(QUrl("http://www.example.com/api/users"));
    9 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); // 设置 Content-Type 为 JSON
    10 QNetworkReply *reply = networkManager->post(request, requestBody);
    11
    12 // 发送表单数据 (application/x-www-form-urlencoded)
    13 QUrlQuery formData;
    14 formData.addQueryItem("username", "testuser");
    15 formData.addQueryItem("password", "password123");
    16 QByteArray requestBody = formData.toString(QUrl::FullyEncoded).toUtf8(); // 编码为 URL 格式
    17 QNetworkRequest request(QUrl("http://www.example.com/api/login"));
    18 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); // 设置 Content-Type
    19 QNetworkReply *reply = networkManager->post(request, requestBody);

    Content-Type 请求头
    ▮▮▮▮Content-Type 请求头非常重要,它告知服务器请求体数据的格式。常用的 Content-Type 值包括:
    ▮▮▮▮⚝ application/json:JSON 数据
    ▮▮▮▮⚝ application/x-www-form-urlencoded:表单数据
    ▮▮▮▮⚝ multipart/form-data:用于上传文件
    ▮▮▮▮⚝ text/plain:纯文本数据
    ▮▮▮▮⚝ text/xmlapplication/xml:XML (可扩展标记语言) 数据

    9.4.3 HTTP 响应的处理:状态码 (Status Code), 响应头 (Headers), 响应体 (Body)

    HTTP 响应包含了服务器对请求的回复信息。QNetworkReply 对象提供了访问 HTTP 响应状态码、响应头和响应体数据的方法。

    获取状态码 (Status Code)
    ▮▮▮▮HTTP 状态码用于表示服务器对请求的处理结果。常用的状态码包括:
    ▮▮▮▮⚝ 200 OK:请求成功
    ▮▮▮▮⚝ 201 Created:资源已创建成功
    ▮▮▮▮⚝ 400 Bad Request:客户端请求错误
    ▮▮▮▮⚝ 401 Unauthorized:未授权
    ▮▮▮▮⚝ 403 Forbidden:禁止访问
    ▮▮▮▮⚝ 404 Not Found:资源未找到
    ▮▮▮▮⚝ 500 Internal Server Error:服务器内部错误

    使用 attribute(QNetworkRequest::HttpStatusCodeAttribute) 函数获取状态码。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QNetworkReply *reply; // HTTP 响应 reply 对象
    2 int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
    3 qDebug() << "HTTP status code: " << statusCode;
    4 if (statusCode == 200) {
    5 // 请求成功
    6 } else if (statusCode >= 400 && statusCode < 500) {
    7 // 客户端错误
    8 } else if (statusCode >= 500) {
    9 // 服务器错误
    10 }

    获取响应头 (Headers)
    ▮▮▮▮HTTP 响应头包含了服务器返回的额外信息,例如 Content-Type, Content-Length, Date, Server 等。使用 rawHeaderList() 函数获取所有响应头名称,使用 rawHeader() 函数获取指定名称的响应头值。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QNetworkReply *reply; // HTTP 响应 reply 对象
    2 QList<QByteArray> headers = reply->rawHeaderList();
    3 for (const QByteArray &headerName : headers) {
    4 QByteArray headerValue = reply->rawHeader(headerName);
    5 qDebug() << "Response header: " << headerName << ": " << headerValue;
    6 }
    7 QByteArray contentTypeHeader = reply->rawHeader("Content-Type"); // 获取 Content-Type 响应头

    获取响应体 (Body) 数据
    ▮▮▮▮HTTP 响应体包含了服务器返回的实际数据,例如 HTML 页面、JSON 数据、图片文件等。使用 readAll() 函数读取响应体数据。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QNetworkReply *reply; // HTTP 响应 reply 对象
    2 QByteArray responseBody = reply->readAll();
    3 qDebug() << "Response body: " << responseBody;
    4 // 处理响应体数据,例如解析 JSON、显示 HTML 等

    9.4.4 处理 JSON 数据

    JSON (JavaScript Object Notation) 是一种轻量级的数据交换格式,常用于 Web API 的数据传输。Qt 提供了 QJsonDocument, QJsonObject, QJsonArray 等类,方便处理 JSON 数据。

    解析 JSON 响应
    ▮▮▮▮当 HTTP 响应的 Content-Typeapplication/json 时,响应体数据通常是 JSON 格式的。可以使用 QJsonDocument::fromJson() 函数将 JSON 字符串解析为 QJsonDocument 对象,然后通过 QJsonDocument 对象访问 JSON 数据。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QNetworkReply *reply; // HTTP 响应 reply 对象
    2 QByteArray responseBody = reply->readAll();
    3 QJsonParseError jsonError;
    4 QJsonDocument jsonDoc = QJsonDocument::fromJson(responseBody, &jsonError); // 解析 JSON
    5 if (jsonError.error == QJsonParseError::NoError) {
    6 if (jsonDoc.isObject()) {
    7 QJsonObject jsonObj = jsonDoc.object();
    8 // 处理 JSON 对象
    9 QString name = jsonObj["name"].toString();
    10 int age = jsonObj["age"].toInt();
    11 qDebug() << "JSON name: " << name << ", age: " << age;
    12 } else if (jsonDoc.isArray()) {
    13 QJsonArray jsonArray = jsonDoc.array();
    14 // 处理 JSON 数组
    15 for (const QJsonValue &value : jsonArray) {
    16 qDebug() << "JSON array item: " << value;
    17 }
    18 }
    19 } else {
    20 qDebug() << "JSON parse error: " << jsonError.errorString();
    21 // JSON 解析错误处理
    22 }

    构建 JSON 请求体
    ▮▮▮▮当需要发送 JSON 数据作为 HTTP 请求体时,可以使用 QJsonObjectQJsonArray 构建 JSON 数据,然后使用 QJsonDocument::toJson() 函数将 JSON 对象或数组转换为 JSON 字符串,再将 JSON 字符串作为请求体数据发送。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QJsonObject jsonData;
    2 jsonData["username"] = "testuser";
    3 jsonData["password"] = "password123";
    4 QJsonDocument jsonDoc(jsonData);
    5 QByteArray requestBody = jsonDoc.toJson(); // 转换为 JSON 字符串
    6
    7 QNetworkRequest request(QUrl("http://www.example.com/api/login"));
    8 request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
    9 QNetworkReply *reply = networkManager->post(request, requestBody);

    通过 Qt 提供的 JSON 支持库,可以方便地在 HTTP 编程中处理 JSON 数据,实现与 Web API 的数据交互。

    10. 数据存储与数据库:Qt SQL 模块应用

    本章介绍 Qt 的数据存储与数据库编程 (Database Programming),讲解如何使用 Qt SQL 模块 (Qt SQL Module) 访问和操作数据库。

    10.1 Qt SQL 模块 (Qt SQL Module) 概述

    本节将概述 Qt SQL 模块,包括其功能、支持的数据库类型以及常用的类,为后续深入学习 Qt 数据库编程打下基础。

    10.1.1 Qt SQL 模块的功能:数据库连接、SQL 查询、事务处理

    Qt SQL 模块是 Qt 框架中用于数据库编程的重要组成部分,它提供了一系列类和接口,使得 C++ 应用程序能够方便、高效地与各种类型的数据库进行交互。Qt SQL 模块的核心功能主要体现在以下几个方面:

    数据库连接 (Database Connection)
    ▮ Qt SQL 模块允许应用程序与多种类型的数据库建立连接。这包括常见的桌面数据库如 SQLite,以及流行的服务器数据库如 MySQL、PostgreSQL 等。
    ▮ 通过 QSqlDatabase 类,可以方便地添加、管理和维护数据库连接。每个 QSqlDatabase 对象代表一个数据库连接,应用程序可以通过它与特定的数据库进行通信。
    ▮ Qt 提供了统一的接口来处理不同数据库的连接,使得开发者可以使用相似的代码来连接和操作不同类型的数据库,提高了代码的可移植性和可维护性。

    SQL 查询 (SQL Query)
    ▮ Qt SQL 模块提供了执行 SQL (Structured Query Language) 语句的能力。SQL 是用于管理关系数据库的标准语言,通过 SQL 可以进行数据的查询、插入、更新和删除等操作 (CRUD - Create, Read, Update, Delete)。
    QSqlQuery 类是执行 SQL 查询的核心类。开发者可以使用 QSqlQuery 对象来执行各种 SQL 语句,并获取查询结果。
    ▮ Qt SQL 模块支持标准的 SQL 语法,同时也针对不同数据库的特性提供了一些扩展功能。这使得开发者能够充分利用 SQL 的强大功能来操作数据库。

    事务处理 (Transaction Processing)
    ▮ 事务 (Transaction) 是一系列数据库操作的逻辑单元,它要么全部成功执行,要么全部回滚,保证了数据库操作的原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和持久性 (Durability),即 ACID 特性。
    ▮ Qt SQL 模块提供了事务处理的支持,允许开发者将一系列数据库操作封装在一个事务中。通过 QSqlDatabase 类的 transaction()commit()rollback() 函数,可以方便地控制事务的开始、提交和回滚。
    ▮ 事务处理对于保证数据完整性和一致性至关重要,特别是在处理涉及多个数据表或复杂业务逻辑的数据库操作时。

    总而言之,Qt SQL 模块通过提供数据库连接、SQL 查询和事务处理等核心功能,为 C++ 开发者提供了一套强大而灵活的数据库编程工具,使得开发跨平台、高性能的数据库应用程序成为可能。

    10.1.2 支持的数据库类型:SQLite, MySQL, PostgreSQL, ODBC, TDS

    Qt SQL 模块的一个重要优势在于其对多种数据库类型的广泛支持。通过使用不同的数据库驱动 (Driver),Qt 应用程序可以连接到各种流行的数据库系统。以下是 Qt SQL 模块常用的支持数据库类型:

    SQLite
    ▮ SQLite 是一款轻量级的嵌入式数据库引擎。它以文件形式存储数据,无需独立的服务器进程,非常适合于桌面应用、移动应用和嵌入式系统等场景。
    ▮ Qt 对 SQLite 提供了良好的支持,通过内置的 SQLite 驱动,可以方便地在 Qt 应用程序中使用 SQLite 数据库。
    ▮ 由于 SQLite 的轻便性和易用性,它常被用作学习数据库编程的入门选择,以及小型应用程序的数据存储方案。

    MySQL
    ▮ MySQL 是一款流行的开源关系型数据库管理系统 (RDBMS)。它具有高性能、高可靠性和易管理性等特点,广泛应用于 Web 应用、企业级应用等领域。
    ▮ Qt 提供了 MySQL 驱动,允许 Qt 应用程序连接到 MySQL 服务器,并进行各种数据库操作。
    ▮ 使用 MySQL 驱动通常需要安装 MySQL 客户端库,并在 Qt 项目中配置相应的驱动。

    PostgreSQL
    ▮ PostgreSQL 也是一款强大的开源关系型数据库管理系统。它以其高度的可扩展性、数据完整性和符合 SQL 标准而闻名,常用于大型企业级应用和数据仓库等场景。
    ▮ Qt 同样提供了 PostgreSQL 驱动,支持 Qt 应用程序与 PostgreSQL 服务器的连接和交互。
    ▮ 类似于 MySQL,使用 PostgreSQL 驱动也需要安装 PostgreSQL 客户端库并进行相应的配置。

    ODBC (Open Database Connectivity)
    ▮ ODBC 是一种开放的数据库互连标准。它允许应用程序通过 ODBC 驱动程序访问各种数据库系统,提供了一种通用的数据库访问接口。
    ▮ Qt SQL 模块支持 ODBC 驱动,使得 Qt 应用程序能够连接到任何提供了 ODBC 驱动的数据库,例如 Microsoft SQL Server, Oracle Database 等。
    ▮ 通过 ODBC,Qt 应用程序可以实现更广泛的数据库兼容性,但通常需要配置 ODBC 数据源 (DSN - Data Source Name)。

    TDS (Tabular Data Stream)
    ▮ TDS 协议是 Microsoft SQL Server 和 Sybase SQL Server 使用的专有协议。Qt SQL 模块提供了 TDS 驱动 (通常称为 QODBC 驱动,并配置为使用 TDS 协议),用于连接到 Microsoft SQL Server 和 Sybase SQL Server 数据库。
    ▮ 使用 TDS 驱动允许 Qt 应用程序与 Microsoft SQL Server 等企业级数据库进行集成。
    ▮ 与 ODBC 类似,连接 TDS 数据库通常也需要配置 ODBC 数据源,并指定使用 TDS 协议。

    除了上述列出的数据库类型,Qt SQL 模块还可能支持其他数据库,例如 Oracle (通过 ODBC 或 Oracle Call Interface (OCI) 驱动) 和 InterBase/Firebird (通过 InterBase 驱动或 ODBC 驱动)。具体的支持情况和驱动程序可能取决于 Qt 版本和平台。

    开发者在选择数据库类型时,需要根据应用程序的需求、性能要求、部署环境以及已有的数据库基础设施等因素进行综合考虑。Qt SQL 模块的跨数据库支持特性,为开发者提供了更大的灵活性和选择空间。

    10.1.3 常用 SQL 类:QSqlDatabase, QSqlQuery, QSqlTableModel, QSqlRelationalTableModel

    Qt SQL 模块提供了一系列类来简化数据库编程,这些类分别负责不同的任务,共同构建起 Qt 数据库编程的框架。以下是 Qt SQL 模块中一些最常用的类及其用途:

    QSqlDatabase
    QSqlDatabase 类是 Qt SQL 模块的核心类,用于表示数据库连接。每个 QSqlDatabase 对象代表一个与特定数据库的连接会话。
    ▮ 主要用途:
    ▮▮▮▮ⓐ 建立数据库连接:通过 QSqlDatabase::addDatabase() 静态函数,可以创建并添加一个数据库连接。需要指定数据库类型 (如 "QSQLITE", "QMYSQL", "QPSQL", "QODBC", "QTDS") 和连接名称 (用于标识连接)。
    ▮▮▮▮ⓑ 配置连接参数:可以设置数据库连接的各种参数,例如数据库名称、主机名、端口号、用户名、密码等。这些参数的具体设置取决于数据库类型。
    ▮▮▮▮ⓒ 管理数据库连接QSqlDatabase 类提供了一些函数来管理数据库连接,如 open() (打开连接), close() (关闭连接), isOpen() (检查连接是否打开), databaseName() (获取数据库名称) 等。
    ▮▮▮▮ⓓ 事务处理QSqlDatabase 类提供了事务处理的接口,包括 transaction() (开始事务), commit() (提交事务), rollback() (回滚事务) 等函数。

    QSqlQuery
    QSqlQuery 类用于执行 SQL 查询语句并获取结果。它是执行 SQL 命令和操作数据库的主要工具。
    ▮ 主要用途:
    ▮▮▮▮ⓐ 执行 SQL 语句:通过 QSqlQuery::exec() 函数,可以执行各种 SQL 语句,包括 SELECT (查询), INSERT (插入), UPDATE (更新), DELETE (删除), CREATE TABLE (创建表) 等。
    ▮▮▮▮ⓑ 处理查询结果:对于 SELECT 查询,QSqlQuery 提供了遍历结果集的方法。可以使用 next() 函数移动到结果集的下一行,并使用 value(int index) 函数获取当前行指定列的值。
    ▮▮▮▮ⓒ 预处理语句QSqlQuery 支持预处理语句 (Prepared Statements),可以提高 SQL 执行效率和安全性。可以使用 prepare() 函数预编译 SQL 语句,然后使用 bindValue() 函数绑定参数值,最后使用 exec() 函数执行。
    ▮▮▮▮ⓓ 错误处理QSqlQuery 提供了 lastError() 函数来获取最后一次 SQL 操作的错误信息,方便进行错误处理和调试。

    QSqlTableModel
    QSqlTableModel 类提供了一个可编辑的 SQL 数据模型的实现,用于将数据库表的数据以表格形式展示在视图 (如 QTableView) 中,并允许用户编辑数据。
    ▮ 主要用途:
    ▮▮▮▮ⓐ 作为数据模型QSqlTableModel 可以作为 Qt 模型/视图架构中的模型 (Model),与视图 (View) 组件 (如 QTableView) 配合使用,显示数据库表的数据。
    ▮▮▮▮ⓑ 数据浏览与编辑:通过 QSqlTableModel,用户可以直接在视图中浏览和编辑数据库表的数据。模型的修改会自动反映到数据库中。
    ▮▮▮▮ⓒ 数据操作QSqlTableModel 提供了插入新行、删除行、修改数据等操作的接口。例如,可以使用 insertRows(), removeRows(), setData() 等函数修改模型数据。
    ▮▮▮▮ⓓ 数据提交与回滚QSqlTableModel 提供了 submitAll() 函数将模型中的修改提交到数据库,以及 revertAll() 函数回滚所有未提交的修改。

    QSqlRelationalTableModel
    QSqlRelationalTableModel 类继承自 QSqlTableModel,扩展了对关联表 (Relational Table) 的支持。它特别适用于处理包含外键 (Foreign Key) 关系的数据库表。
    ▮ 主要用途:
    ▮▮▮▮ⓐ 处理关联数据QSqlRelationalTableModel 可以自动处理外键关系,在视图中以友好的方式展示关联表的数据。例如,可以将外键字段显示为关联表中对应行的某个字段值,而不是原始的外键 ID。
    ▮▮▮▮ⓑ 外键关联编辑:在编辑包含外键的数据时,QSqlRelationalTableModel 可以提供下拉列表等控件,方便用户从关联表中选择外键值。
    ▮▮▮▮ⓒ 设置关联关系:通过 setRelation(int column, const QSqlRelation &relation) 函数,可以为指定的列设置外键关联关系。QSqlRelation 对象描述了外键列与关联表之间的关系。

    这些类是 Qt SQL 模块中最核心和常用的类。在实际的数据库应用程序开发中,开发者通常会结合使用这些类,以及 Qt SQL 模块提供的其他辅助类,来实现各种数据库操作和用户界面功能。例如,可以使用 QDataWidgetMapper 类将模型的数据映射到表单控件,使用 QSqlError 类处理数据库错误,等等。掌握这些类的用法是进行 Qt 数据库编程的关键。

    10.2 数据库连接与配置

    本节将详细讲解如何使用 Qt SQL 模块建立和配置数据库连接。数据库连接是进行任何数据库操作的基础,正确的连接配置是保证应用程序能够成功访问数据库的前提。

    10.2.1 QSqlDatabase 的使用:添加数据库连接

    在 Qt SQL 模块中,QSqlDatabase 类负责管理数据库连接。要使用数据库,首先需要创建一个 QSqlDatabase 对象并配置连接参数。通常,我们不直接创建 QSqlDatabase 对象,而是使用其静态函数 addDatabase() 来添加一个新的数据库连接。

    QSqlDatabase::addDatabase() 函数有多个重载版本,最常用的版本是:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 static QSqlDatabase addDatabase(const QString &type, const QString &connectionName = QLatin1String(defaultConnection));

    参数说明:

    type:指定要连接的数据库类型。这是一个字符串,表示数据库驱动的名称。常用的驱动名称包括:
    ▮▮▮▮⚝ "QSQLITE":SQLite 数据库
    ▮▮▮▮⚝ "QMYSQL":MySQL 数据库
    ▮▮▮▮⚝ "QPSQL":PostgreSQL 数据库
    ▮▮▮▮⚝ "QODBC":ODBC 数据库
    ▮▮▮▮⚝ "QTDS":TDS (Microsoft SQL Server, Sybase SQL Server) 数据库

    完整的驱动名称列表可以参考 Qt 官方文档。

    connectionName:连接名称。这是一个可选参数,用于为数据库连接指定一个唯一的名称。如果省略此参数,将使用默认连接名称 "defaultConnection"。如果需要管理多个数据库连接,建议为每个连接指定不同的名称。

    QSqlDatabase::addDatabase() 函数的返回值是一个指向新创建的 QSqlDatabase 对象的指针。如果创建失败 (例如,指定的数据库驱动不可用),则返回空指针 nullptr

    使用步骤

    包含头文件:首先,需要在 C++ 代码中包含 QSqlDatabase 类的头文件:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QSqlDatabase>

    添加数据库连接:在代码中调用 QSqlDatabase::addDatabase() 函数,指定数据库类型和连接名称 (可选)。例如,要添加一个 SQLite 数据库连接,可以使用以下代码:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    2 if (!db.isValid()) {
    3 qDebug() << "Failed to add database connection!";
    4 // 错误处理
    5 return;
    6 }

    这段代码尝试添加一个 SQLite 数据库连接,并使用默认的连接名称。QSqlDatabase::isValid() 函数用于检查返回的 QSqlDatabase 对象是否有效。如果添加数据库连接失败,则输出错误信息并返回。

    设置数据库名称 (对于 SQLite):对于 SQLite 数据库,需要设置数据库文件的路径。可以使用 QSqlDatabase::setDatabaseName() 函数来设置数据库文件名。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 db.setDatabaseName("mydatabase.db"); // 设置 SQLite 数据库文件名

    对于其他类型的数据库 (如 MySQL, PostgreSQL),数据库名称通常是在连接参数中指定的,而不是通过 setDatabaseName() 函数。

    设置连接参数 (对于 MySQL, PostgreSQL 等):对于服务器数据库 (如 MySQL, PostgreSQL),需要设置更多的连接参数,例如主机名、端口号、用户名、密码等。可以使用 QSqlDatabase 类的以下函数来设置这些参数:

    setHostName(const QString &hostName):设置主机名或 IP 地址。
    setPort(int port):设置端口号。
    setUserName(const QString &userName):设置用户名。
    setPassword(const QString &password):设置密码。

    例如,要连接到 MySQL 数据库,可以使用以下代码:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
    2 if (!db.isValid()) {
    3 qDebug() << "Failed to add MySQL database connection!";
    4 return;
    5 }
    6 db.setHostName("localhost"); // 设置主机名
    7 db.setPort(3306); // 设置端口号
    8 db.setUserName("username"); // 设置用户名
    9 db.setPassword("password"); // 设置密码
    10 db.setDatabaseName("mydatabase"); // 设置数据库名称

    打开数据库连接:配置完连接参数后,需要调用 QSqlDatabase::open() 函数来打开数据库连接。open() 函数返回 true 如果连接成功,返回 false 如果连接失败。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 if (!db.open()) {
    2 qDebug() << "Failed to open database: " << db.lastError().text();
    3 // 错误处理
    4 return;
    5 }
    6 qDebug() << "Database connection opened successfully!";

    如果连接失败,可以使用 QSqlDatabase::lastError() 函数获取详细的错误信息,方便进行错误诊断。

    关闭数据库连接:当不再需要使用数据库连接时,应该调用 QSqlDatabase::close() 函数来关闭连接,释放资源。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 db.close();
    2 qDebug() << "Database connection closed.";

    示例代码 (连接 SQLite 数据库)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QCoreApplication>
    2 #include <QDebug>
    3 #include <QSqlDatabase>
    4 #include <QSqlError>
    5
    6 int main(int argc, char *argv[])
    7 {
    8 QCoreApplication a(argc, argv);
    9
    10 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    11 if (!db.isValid()) {
    12 qDebug() << "Failed to add database connection!";
    13 return -1;
    14 }
    15 db.setDatabaseName("mydatabase.db");
    16
    17 if (!db.open()) {
    18 qDebug() << "Failed to open database: " << db.lastError().text();
    19 return -1;
    20 }
    21 qDebug() << "Database connection opened successfully!";
    22
    23 // 在这里执行数据库操作...
    24
    25 db.close();
    26 qDebug() << "Database connection closed.";
    27
    28 return a.exec();
    29 }

    这段代码演示了如何添加、配置、打开和关闭一个 SQLite 数据库连接。对于其他类型的数据库,只需修改 addDatabase() 函数的第一个参数 (数据库类型) 和相应的连接参数即可。

    10.2.2 数据库驱动 (Driver) 的选择与加载

    数据库驱动 (Driver) 是 Qt SQL 模块连接不同类型数据库的关键组件。Qt 应用程序需要加载相应的数据库驱动,才能与特定类型的数据库进行通信。

    驱动选择

    在调用 QSqlDatabase::addDatabase(const QString &type, ...) 函数时,第一个参数 type 就是指定数据库驱动的名称。常用的驱动名称包括:

    "QSQLITE":SQLite 驱动
    "QMYSQL":MySQL 驱动
    "QPSQL":PostgreSQL 驱动
    "QODBC":ODBC 驱动
    "QTDS":TDS 驱动

    选择正确的驱动名称非常重要,它必须与要连接的数据库类型相匹配。例如,要连接 SQLite 数据库,必须使用 "QSQLITE" 驱动;要连接 MySQL 数据库,必须使用 "QMYSQL" 驱动,以此类推。

    驱动加载

    Qt SQL 模块的数据库驱动通常是以插件 (Plugin) 的形式提供的。应用程序在运行时需要加载这些驱动插件,才能使用相应的数据库功能。驱动插件的加载方式可以是静态加载动态加载

    静态加载 (Static Loading)

    在静态构建 Qt 应用程序时,可以将数据库驱动静态链接到应用程序中。这意味着驱动代码会直接编译到可执行文件中,应用程序启动时会自动加载驱动,无需额外的加载步骤。

    静态加载的优点是简单、方便,应用程序运行时不需要额外的驱动文件。缺点是可执行文件体积会增大,且不支持在运行时动态选择和切换驱动。

    要静态加载驱动,需要在编译 Qt 应用程序时,将相应的驱动模块 (如 qtbase/src/plugins/sqldrivers/sqlite) 包含到构建配置中。具体的配置方法取决于使用的构建系统 (如 qmake, CMake)。

    动态加载 (Dynamic Loading)

    在动态构建 Qt 应用程序时 (通常是默认方式),数据库驱动以动态库 (DLL on Windows, .so on Linux, .dylib on macOS) 的形式存在。应用程序在运行时需要动态加载这些驱动库。

    动态加载的优点是可执行文件体积较小,且可以在运行时动态选择和切换驱动。缺点是需要确保驱动库文件在应用程序运行时能够被找到。

    Qt 应用程序在运行时会按照一定的搜索路径查找驱动插件。默认情况下,Qt 会在以下位置搜索驱动插件:

    ▮▮▮▮⚝ 应用程序的插件目录 (通常是应用程序可执行文件所在目录下的 plugins/sqldrivers 子目录)。
    ▮▮▮▮⚝ Qt 安装目录下的插件目录 (例如,<Qt安装目录>/plugins/sqldrivers)。
    ▮▮▮▮⚝ 系统插件目录 (例如,/usr/lib/qt5/plugins/sqldrivers on Linux)。

    为了确保应用程序能够找到并加载驱动插件,需要将相应的驱动库文件复制到上述搜索路径中的某个位置。通常的做法是将驱动库文件复制到应用程序可执行文件所在目录下的 plugins/sqldrivers 子目录中。

    常见驱动库文件名

    ▮▮▮▮⚝ SQLite 驱动:qsqlite.dll (Windows), libqsqlite.so (Linux), libqsqlite.dylib (macOS)
    ▮▮▮▮⚝ MySQL 驱动:qmysql.dll (Windows), libqmysql.so (Linux), libqmysql.dylib (macOS)
    ▮▮▮▮⚝ PostgreSQL 驱动:qpgsql.dll (Windows), libqpgsql.so (Linux), libqpgsql.dylib (macOS)
    ▮▮▮▮⚝ ODBC 驱动:qodbc.dll (Windows), libqodbc.so (Linux), libqodbc.dylib (macOS)
    ▮▮▮▮⚝ TDS 驱动:qodbc.dll (Windows), libqodbc.so (Linux), libqodbc.dylib (macOS) (通常 ODBC 驱动也用于 TDS 连接,只是连接字符串不同)

    检查驱动是否可用

    在添加数据库连接之前,可以使用 QSqlDatabase::drivers() 静态函数来获取 Qt SQL 模块支持的所有可用驱动的列表。通过检查返回的列表中是否包含所需的驱动名称,可以判断驱动是否可用。

    例如,要检查 SQLite 驱动是否可用,可以使用以下代码:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QStringList drivers = QSqlDatabase::drivers();
    2 if (drivers.contains("QSQLITE")) {
    3 qDebug() << "SQLite driver is available.";
    4 } else {
    5 qDebug() << "SQLite driver is NOT available!";
    6 // 错误处理
    7 }

    如果需要的驱动不可用,可能是驱动插件没有正确安装或加载。需要检查驱动插件的安装和配置,确保驱动库文件在应用程序的插件搜索路径中。

    总结

    选择正确的数据库驱动名称,并确保驱动插件能够被 Qt 应用程序正确加载,是成功建立数据库连接的关键步骤。在动态部署应用程序时,务必将所需的驱动库文件与应用程序一起发布,放置在正确的插件目录下,以保证应用程序在目标平台上能够正常连接数据库。

    10.2.3 连接参数的配置:主机名、端口号、用户名、密码、数据库名

    配置数据库连接参数是建立数据库连接的必要步骤。不同的数据库类型需要配置不同的连接参数。对于服务器数据库 (如 MySQL, PostgreSQL, SQL Server),通常需要配置以下参数:

    主机名 (Hostname)

    主机名或 IP 地址指定了数据库服务器所在的计算机。

    ▮▮▮▮⚝ 对于本地数据库服务器,可以使用 "localhost""127.0.0.1"
    ▮▮▮▮⚝ 对于远程数据库服务器,需要使用服务器的实际主机名或 IP 地址。

    使用 QSqlDatabase::setHostName(const QString &hostName) 函数设置主机名。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 db.setHostName("localhost"); // 连接本地数据库服务器
    2 db.setHostName("192.168.1.100"); // 连接 IP 地址为 192.168.1.100 的远程服务器
    3 db.setHostName("db.example.com"); // 连接主机名为 db.example.com 的远程服务器

    端口号 (Port)

    端口号指定了数据库服务器监听的网络端口。每种数据库服务器都有默认的端口号:

    ▮▮▮▮⚝ MySQL 默认端口号:3306
    ▮▮▮▮⚝ PostgreSQL 默认端口号:5432
    ▮▮▮▮⚝ SQL Server 默认端口号:1433

    如果数据库服务器使用了非默认端口号,需要显式设置端口号。使用 QSqlDatabase::setPort(int port) 函数设置端口号。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 db.setPort(3306); // 连接 MySQL 默认端口
    2 db.setPort(5433); // 连接 PostgreSQL 的 5433 端口

    用户名 (Username)

    用户名是用于身份验证的数据库用户账户名。连接数据库需要提供有效的用户名。

    使用 QSqlDatabase::setUserName(const QString &userName) 函数设置用户名。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 db.setUserName("myuser"); // 使用用户名为 myuser 的账户

    密码 (Password)

    密码是与用户名对应的数据库用户账户密码,用于身份验证。

    使用 QSqlDatabase::setPassword(const QString &password) 函数设置密码。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 db.setPassword("mypassword"); // 使用密码为 mypassword 的账户

    安全提示:在实际应用中,应妥善保管数据库密码,避免硬编码在代码中。可以考虑使用配置文件、环境变量或密钥管理系统等方式来安全地管理密码。

    数据库名 (Database Name)

    数据库名指定了要连接的具体数据库实例。在一个数据库服务器上,可能存在多个数据库实例。

    使用 QSqlDatabase::setDatabaseName(const QString &dbName) 函数设置数据库名。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 db.setDatabaseName("mydatabase"); // 连接名为 mydatabase 的数据库

    对于 SQLite 数据库,数据库名实际上是数据库文件的路径。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 db.setDatabaseName("mydatabase.db"); // SQLite 数据库文件名
    2 db.setDatabaseName("/path/to/mydatabase.db"); // SQLite 数据库文件路径
    3 db.setDatabaseName(":memory:"); // SQLite 内存数据库

    ":memory:" 是 SQLite 特殊的数据库名,表示创建一个内存数据库。内存数据库的数据只在内存中存在,程序结束后数据会丢失。

    其他连接参数

    除了上述常用参数,QSqlDatabase 类还提供了一些其他连接参数的设置函数,例如:

    setConnectOptions(const QString &options):设置数据库连接选项,例如字符集、连接超时等。具体的选项取决于数据库驱动。
    setDatabaseTemplate(const QString &templateName):设置数据库模板 (某些数据库支持)。

    连接参数的具体配置方法和可用选项,需要参考所使用数据库驱动的 Qt 文档和数据库自身的文档。

    示例代码 (连接 MySQL 数据库)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
    2 db.setHostName("localhost");
    3 db.setPort(3306);
    4 db.setUserName("myuser");
    5 db.setPassword("mypassword");
    6 db.setDatabaseName("mydatabase");
    7
    8 if (!db.open()) {
    9 qDebug() << "Failed to open MySQL database: " << db.lastError().text();
    10 // 错误处理
    11 } else {
    12 qDebug() << "MySQL database connection opened successfully!";
    13 // 数据库操作...
    14 db.close();
    15 }

    这段代码演示了如何配置连接 MySQL 数据库所需的各种参数,并打开数据库连接。根据实际的数据库服务器配置,修改相应的主机名、端口号、用户名、密码和数据库名即可。

    10.2.4 错误处理与连接状态检查

    在数据库连接和操作过程中,可能会出现各种错误,例如连接失败、SQL 语句执行错误等。良好的错误处理机制对于保证应用程序的健壮性和可靠性至关重要。同时,及时检查数据库连接状态,可以避免在连接失效的情况下进行数据库操作。

    错误处理

    Qt SQL 模块提供了 QSqlError 类来表示数据库操作过程中发生的错误。QSqlDatabaseQSqlQuery 等类都提供了 lastError() 函数,用于获取最后一次数据库操作的错误信息。

    QSqlError::lastError() 函数返回一个 QSqlError 对象。QSqlError 类提供了一些函数来获取错误的详细信息:

    type():返回错误类型,QSqlError::ErrorType 枚举类型,例如 NoError, ConnectionError, StatementError 等。
    databaseText():返回数据库特定的错误描述文本。
    driverText():返回数据库驱动特定的错误描述文本。
    text():返回错误描述文本 (通常是 databaseText()driverText(),取决于错误类型)。
    number():返回数据库特定的错误代码 (整数)。

    示例代码 (错误处理)

    在打开数据库连接时,如果 QSqlDatabase::open() 函数返回 false,表示连接失败。可以使用 QSqlDatabase::lastError() 获取错误信息并进行处理:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    2 db.setDatabaseName("nonexistent_database.db"); // 故意设置一个不存在的数据库文件
    3
    4 if (!db.open()) {
    5 QSqlError error = db.lastError();
    6 qDebug() << "Database connection error:";
    7 qDebug() << " Type:" << error.type();
    8 qDebug() << " Database Text:" << error.databaseText();
    9 qDebug() << " Driver Text:" << error.driverText();
    10 qDebug() << " Error Text:" << error.text();
    11 qDebug() << " Error Number:" << error.number();
    12 // 错误处理逻辑,例如显示错误信息给用户,记录日志等
    13 } else {
    14 qDebug() << "Database connection opened successfully!";
    15 // 数据库操作...
    16 db.close();
    17 }

    同样地,在执行 SQL 查询时,如果 QSqlQuery::exec() 函数返回 false,也表示 SQL 语句执行失败。可以使用 QSqlQuery::lastError() 获取错误信息:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query(db);
    2 QString sql = "SELECT * FROM nonexistent_table"; // 故意查询一个不存在的表
    3 if (!query.exec(sql)) {
    4 QSqlError error = query.lastError();
    5 qDebug() << "SQL query error:";
    6 qDebug() << " Error Text:" << error.text();
    7 qDebug() << " SQL Statement:" << sql;
    8 // 错误处理逻辑
    9 } else {
    10 // 处理查询结果...
    11 }

    在处理数据库错误时,可以根据 QSqlError::type() 返回的错误类型,采取不同的处理策略。例如,对于连接错误 (ConnectionError),可能是数据库服务器未启动、网络连接问题或连接参数配置错误;对于语句错误 (StatementError),可能是 SQL 语句语法错误、表或列不存在、权限不足等问题。

    连接状态检查

    在进行数据库操作之前,应该检查数据库连接是否处于打开状态。可以使用 QSqlDatabase::isOpen() 函数来检查连接状态。isOpen() 函数返回 true 如果连接已打开,返回 false 如果连接已关闭或连接失败。

    示例代码 (连接状态检查)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    2 db.setDatabaseName("mydatabase.db");
    3
    4 if (!db.isOpen()) { // 检查连接是否已打开,避免重复打开
    5 if (!db.open()) {
    6 qDebug() << "Failed to open database: " << db.lastError().text();
    7 return;
    8 } else {
    9 qDebug() << "Database connection opened.";
    10 }
    11 }
    12
    13 if (db.isOpen()) { // 再次检查连接是否打开,确保在进行数据库操作前连接是有效的
    14 // 进行数据库操作...
    15 qDebug() << "Performing database operations...";
    16 // ...
    17 } else {
    18 qDebug() << "Database connection is not open. Cannot perform operations.";
    19 }
    20
    21 db.close();

    在应用程序的生命周期中,数据库连接可能会因为各种原因而关闭 (例如,网络中断、数据库服务器重启等)。在需要进行数据库操作时,最好先检查连接状态,如果连接已关闭,则尝试重新打开连接。

    总结

    完善的错误处理和连接状态检查机制是构建健壮的数据库应用程序的关键。通过使用 QSqlError 类获取详细的错误信息,并根据错误类型采取相应的处理策略,可以提高应用程序的容错能力。在进行数据库操作前,务必检查数据库连接状态,确保连接有效,避免程序崩溃或数据丢失。

    10.3 SQL 查询与数据操作

    本节将深入讲解如何使用 QSqlQuery 类执行 SQL 查询和数据操作。QSqlQuery 是 Qt SQL 模块中执行 SQL 语句的核心类,通过它可以实现数据的查询 (SELECT)、插入 (INSERT)、更新 (UPDATE) 和删除 (DELETE) 等操作 (CRUD 操作)。

    10.3.1 QSqlQuery 的使用:执行 SQL 语句

    QSqlQuery 类提供了执行 SQL 语句的主要接口。要使用 QSqlQuery,首先需要创建一个 QSqlQuery 对象,并将其与一个 QSqlDatabase 对象关联 (可选,如果不关联,则使用默认数据库连接)。然后,可以使用 exec() 函数执行 SQL 语句。

    创建 QSqlQuery 对象

    可以按照以下方式创建 QSqlQuery 对象:

    无参构造函数:创建一个与默认数据库连接关联的 QSqlQuery 对象。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query; // 使用默认数据库连接

    带数据库连接参数的构造函数:创建一个与指定的 QSqlDatabase 对象关联的 QSqlQuery 对象。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::database("myConnection"); // 获取名为 "myConnection" 的数据库连接
    2 QSqlQuery query(db); // 使用指定的数据库连接

    执行 SQL 语句:exec() 函数

    QSqlQuery::exec() 函数用于执行 SQL 语句。它有多个重载版本,最常用的版本是:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 bool QSqlQuery::exec(const QString &query);

    参数 query 是要执行的 SQL 语句字符串。exec() 函数返回 true 如果 SQL 语句执行成功,返回 false 如果执行失败。如果执行失败,可以使用 lastError() 函数获取错误信息。

    执行 SELECT 查询

    SELECT 语句用于从数据库中查询数据。执行 SELECT 语句后,可以使用 QSqlQuery 对象遍历结果集,并获取查询结果。

    示例代码 (执行 SELECT 查询)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query;
    2 QString sql = "SELECT id, name, age FROM employees WHERE age > :age"; // 使用命名占位符 :age
    3 query.prepare(sql); // 预处理 SQL 语句
    4 query.bindValue(":age", 30); // 绑定参数值
    5 if (!query.exec()) { // 执行查询
    6 qDebug() << "SELECT query error: " << query.lastError().text();
    7 return;
    8 }
    9
    10 qDebug() << "Employees older than 30:";
    11 while (query.next()) { // 遍历结果集
    12 int id = query.value(0).toInt(); // 获取第 1 列 (id) 的值
    13 QString name = query.value(1).toString(); // 获取第 2 列 (name) 的值
    14 int age = query.value(2).toInt(); // 获取第 3 列 (age) 的值
    15 qDebug() << QString(" ID: %1, Name: %2, Age: %3").arg(id).arg(name).arg(age);
    16 }

    这段代码演示了如何执行一个 SELECT 查询,查询 employees 表中年龄大于 30 的员工信息,并遍历结果集,输出员工的 ID、姓名和年龄。使用了预处理语句和命名占位符,提高了 SQL 执行效率和安全性 (将在 10.3.2 节详细介绍预处理语句)。

    执行 INSERT 语句

    INSERT 语句用于向数据库表中插入新的数据行。

    示例代码 (执行 INSERT 语句)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query;
    2 QString sql = "INSERT INTO employees (name, age, department) VALUES (:name, :age, :dept)";
    3 query.prepare(sql);
    4 query.bindValue(":name", "Alice");
    5 query.bindValue(":age", 28);
    6 query.bindValue(":dept", "Sales");
    7
    8 if (!query.exec()) {
    9 qDebug() << "INSERT query error: " << query.lastError().text();
    10 return;
    11 }
    12 qDebug() << "New employee inserted successfully!";

    这段代码向 employees 表中插入一条新的员工记录,姓名 "Alice",年龄 28,部门 "Sales"。同样使用了预处理语句和命名占位符。

    执行 UPDATE 语句

    UPDATE 语句用于修改数据库表中已有的数据行。

    示例代码 (执行 UPDATE 语句)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query;
    2 QString sql = "UPDATE employees SET age = :newAge WHERE id = :id";
    3 query.prepare(sql);
    4 query.bindValue(":newAge", 35);
    5 query.bindValue(":id", 101); // 假设要更新 ID 为 101 的员工记录
    6
    7 if (!query.exec()) {
    8 qDebug() << "UPDATE query error: " << query.lastError().text();
    9 return;
    10 }
    11 qDebug() << "Employee record updated successfully!";

    这段代码将 employees 表中 ID 为 101 的员工记录的年龄更新为 35。

    执行 DELETE 语句

    DELETE 语句用于从数据库表中删除数据行。

    示例代码 (执行 DELETE 语句)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query;
    2 QString sql = "DELETE FROM employees WHERE id = :id";
    3 query.prepare(sql);
    4 query.bindValue(":id", 101); // 假设要删除 ID 为 101 的员工记录
    5
    6 if (!query.exec()) {
    7 qDebug() << "DELETE query error: " << query.lastError().text();
    8 return;
    9 }
    10 qDebug() << "Employee record deleted successfully!";

    这段代码从 employees 表中删除 ID 为 101 的员工记录。

    执行 CREATE TABLE 等 DDL 语句

    除了数据操作语句 (DML - Data Manipulation Language),QSqlQuery::exec() 函数还可以执行数据定义语言 (DDL - Data Definition Language) 语句,例如 CREATE TABLE (创建表), DROP TABLE (删除表), ALTER TABLE (修改表结构) 等。

    示例代码 (执行 CREATE TABLE 语句)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query;
    2 QString sql = "CREATE TABLE departments ("
    3 " id INTEGER PRIMARY KEY AUTOINCREMENT,"
    4 " name VARCHAR(50) NOT NULL,"
    5 " location VARCHAR(100)"
    6 ")";
    7
    8 if (!query.exec(sql)) {
    9 qDebug() << "CREATE TABLE query error: " << query.lastError().text();
    10 return;
    11 }
    12 qDebug() << "Table 'departments' created successfully!";

    这段代码创建了一个名为 departments 的新表,包含 id, name, location 三列。

    总结

    QSqlQuery::exec() 函数是执行各种 SQL 语句的核心函数。通过它可以执行 SELECT, INSERT, UPDATE, DELETE 等数据操作语句,以及 CREATE TABLE, DROP TABLE 等数据定义语句。在执行 SQL 语句时,务必进行错误处理,并根据需要使用预处理语句和参数绑定,以提高 SQL 执行效率和安全性。

    10.3.2 预处理语句 (Prepared Statements):提高性能与安全性

    预处理语句 (Prepared Statements) 是一种在数据库编程中常用的技术,可以提高 SQL 语句的执行效率和安全性。Qt SQL 模块通过 QSqlQuery 类提供了对预处理语句的支持。

    预处理语句的概念

    传统的 SQL 执行流程通常是:应用程序将 SQL 语句字符串发送给数据库服务器,数据库服务器解析、编译 SQL 语句,然后执行,最后返回结果。如果应用程序需要多次执行结构相似但参数不同的 SQL 语句 (例如,批量插入数据),每次执行都需要重复进行 SQL 语句的解析和编译,这会消耗大量的系统资源,降低执行效率。

    预处理语句的工作流程是:

    预编译:应用程序先将带有占位符的 SQL 语句 (称为预处理语句) 发送给数据库服务器。数据库服务器对预处理语句进行解析和编译,生成执行计划,并将编译结果缓存起来。

    参数绑定:在每次执行预处理语句时,应用程序只需要将实际的参数值绑定到预处理语句的占位符上,然后发送执行请求给数据库服务器。

    执行:数据库服务器直接使用之前缓存的执行计划,结合绑定的参数值,执行 SQL 语句,并返回结果。

    由于预处理语句只需要在第一次执行时进行解析和编译,后续执行可以直接重用编译结果,因此可以显著提高 SQL 语句的执行效率,特别是在需要多次执行相似 SQL 语句的场景下。

    预处理语句的优势

    提高性能:预处理语句只需要编译一次,后续执行可以重用编译结果,减少了数据库服务器的解析和编译开销,提高了 SQL 语句的执行效率。

    提高安全性:预处理语句可以有效防止 SQL 注入 (SQL Injection) 攻击。SQL 注入是一种常见的安全漏洞,攻击者通过在应用程序提交的 SQL 语句中注入恶意的 SQL 代码,从而篡改或窃取数据库数据。预处理语句使用占位符来代替实际的参数值,参数值在绑定时会被数据库服务器进行转义处理,从而避免恶意 SQL 代码的注入。

    代码可读性和可维护性:使用预处理语句可以使 SQL 语句和参数值分离,提高了代码的可读性和可维护性。

    Qt 中使用预处理语句

    在 Qt SQL 模块中,可以使用 QSqlQuery 类的 prepare() 函数来预编译 SQL 语句,然后使用 bindValue()bindValues() 函数绑定参数值,最后使用 exec()execBatch() 函数执行预处理语句。

    ① 预编译 SQL 语句:prepare() 函数

    QSqlQuery::prepare() 函数用于预编译 SQL 语句。它接受一个 SQL 语句字符串作为参数,该字符串可以包含占位符。Qt SQL 模块支持两种类型的占位符:

    命名占位符 (Named Placeholders):以冒号 : 开头,后跟占位符名称。例如 :name, :age, :id
    位置占位符 (Positional Placeholders):以问号 ? 表示。例如 ?, ?, ?

    建议使用命名占位符,因为它们更易读和维护。

    示例代码 (使用命名占位符预编译 SQL 语句)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query;
    2 QString sql = "INSERT INTO employees (name, age, department) VALUES (:name, :age, :dept)";
    3 if (!query.prepare(sql)) { // 预编译 SQL 语句
    4 qDebug() << "Prepare SQL statement error: " << query.lastError().text();
    5 return;
    6 }
    7 // SQL 语句预编译成功,可以进行参数绑定和执行

    ② 参数绑定:bindValue() 和 bindValues() 函数

    预编译 SQL 语句后,需要使用 bindValue()bindValues() 函数将实际的参数值绑定到预处理语句的占位符上。

    bindValue(const QString &placeholder, const QVariant &val):将单个参数值绑定到指定的命名占位符或位置占位符。
    bindValues(const QMap<QString, QVariant> &values):一次性绑定多个命名占位符的参数值,参数值以 QMap 形式提供。
    bindValues(const QList<QVariant> &values):一次性绑定多个位置占位符的参数值,参数值以 QList 形式提供。

    QVariant 类是 Qt 中用于表示各种数据类型的通用类,可以存储整数、浮点数、字符串、日期时间等各种类型的值。

    示例代码 (使用 bindValue() 绑定参数值)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 query.bindValue(":name", "Bob"); // 绑定 :name 占位符的值为 "Bob"
    2 query.bindValue(":age", 32); // 绑定 :age 占位符的值为 32
    3 query.bindValue(":dept", "IT"); // 绑定 :dept 占位符的值为 "IT"

    ③ 执行预处理语句:exec() 和 execBatch() 函数

    绑定参数值后,可以使用 exec() 函数执行预处理语句。如果需要批量执行预处理语句 (例如,批量插入多条数据),可以使用 execBatch() 函数。

    exec():执行预处理语句一次。
    execBatch():批量执行预处理语句。在批量执行时,需要多次绑定不同的参数值,然后调用 execBatch() 函数一次性执行所有批次的语句。execBatch() 函数通常比多次调用 exec() 函数效率更高。

    示例代码 (使用 exec() 执行预处理语句)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 if (!query.exec()) { // 执行预处理语句
    2 qDebug() << "Execute prepared statement error: " << query.lastError().text();
    3 return;
    4 }
    5 qDebug() << "New employee inserted using prepared statement!";

    示例代码 (批量插入数据,使用 execBatch() 执行预处理语句)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query;
    2 QString sql = "INSERT INTO employees (name, age, department) VALUES (:name, :age, :dept)";
    3 if (!query.prepare(sql)) {
    4 qDebug() << "Prepare SQL statement error: " << query.lastError().text();
    5 return;
    6 }
    7
    8 // 准备数据
    9 QList<QVariant> names = {"Charlie", "David", "Eve"};
    10 QList<QVariant> ages = {25, 40, 29};
    11 QList<QVariant> departments = {"HR", "Finance", "Marketing"};
    12
    13 // 绑定参数值并添加到批量执行
    14 for (int i = 0; i < names.size(); ++i) {
    15 query.bindValue(":name", names[i]);
    16 query.bindValue(":age", ages[i]);
    17 query.bindValue(":dept", departments[i]);
    18 query.addBindValue(":name", names[i]); // 注意:对于 execBatch(), 需要使用 addBindValue()
    19 query.addBindValue(":age", ages[i]);
    20 query.addBindValue(":dept", departments[i]);
    21 }
    22
    23 if (!query.execBatch()) { // 批量执行预处理语句
    24 qDebug() << "Execute batch prepared statement error: " << query.lastError().text();
    25 return;
    26 }
    27 qDebug() << "Batch insert employees using prepared statement successfully!";

    总结

    使用预处理语句是 Qt 数据库编程中的最佳实践。它可以提高 SQL 语句的执行效率,并有效防止 SQL 注入攻击。在需要多次执行结构相似但参数不同的 SQL 语句时,务必使用预处理语句。对于批量数据操作,可以使用 execBatch() 函数批量执行预处理语句,进一步提高效率。

    10.3.3 获取查询结果:next(), value()

    在执行 SELECT 查询后,QSqlQuery 对象会包含查询结果集。可以使用 next() 函数遍历结果集中的每一行,并使用 value() 函数获取当前行指定列的值。

    遍历结果集:next() 函数

    QSqlQuery::next() 函数用于将结果集的游标移动到下一行。它返回 true 如果成功移动到下一行 (即,还有下一行数据),返回 false 如果已经到达结果集的末尾 (即,没有更多数据)。

    通常在一个 while 循环中使用 next() 函数来遍历整个结果集:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 while (query.next()) {
    2 // 获取当前行的数据...
    3 }

    获取列值:value() 函数

    QSqlQuery::value() 函数用于获取当前行指定列的值。它有多个重载版本,常用的版本是:

    QVariant value(int index) const:根据列索引 (从 0 开始) 获取列值。
    QVariant value(const QString &name) const:根据列名获取列值。

    value() 函数返回一个 QVariant 对象,表示列的值。需要根据实际的数据类型,使用 QVariant 提供的类型转换函数 (如 toInt(), toString(), toDouble(), toDateTime() 等) 将 QVariant 对象转换为所需的数据类型。

    示例代码 (遍历结果集并获取列值)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query;
    2 QString sql = "SELECT id, name, age FROM employees";
    3 if (!query.exec(sql)) {
    4 qDebug() << "SELECT query error: " << query.lastError().text();
    5 return;
    6 }
    7
    8 qDebug() << "All employees:";
    9 while (query.next()) { // 遍历结果集
    10 int id = query.value(0).toInt(); // 获取第 1 列 (id) 的值,转换为 int 类型
    11 QString name = query.value("name").toString(); // 获取列名为 "name" 的值,转换为 QString 类型
    12 int age = query.value(2).toInt(); // 获取第 3 列 (age) 的值,转换为 int 类型
    13 qDebug() << QString(" ID: %1, Name: %2, Age: %3").arg(id).arg(name).arg(age);
    14 }

    这段代码查询 employees 表的所有员工信息,并遍历结果集,输出每个员工的 ID、姓名和年龄。分别使用了列索引 (0, 2) 和列名 ("name") 来获取列值。

    其他获取结果集信息函数

    QSqlQuery 类还提供了一些其他函数,用于获取结果集的信息:

    size():返回结果集中包含的行数。注意:某些数据库驱动可能不支持返回结果集大小,此时 size() 函数可能返回 -1 或需要遍历整个结果集才能确定大小。
    record():返回结果集的元数据信息,即 QSqlRecord 对象,包含了结果集列的名称、类型等信息。
    isNull(int index)isNull(const QString &name):检查指定列的值是否为 NULL。
    seek(int index):将结果集的游标移动到指定的行索引 (从 0 开始)。
    first():将结果集的游标移动到第一行。
    last():将结果集的游标移动到最后一行 (如果驱动支持)。
    previous():将结果集的游标移动到上一行。
    at():返回当前游标所在的行索引 (从 0 开始)。

    示例代码 (获取结果集元数据信息)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlQuery query;
    2 QString sql = "SELECT id, name, age FROM employees";
    3 if (!query.exec(sql)) {
    4 qDebug() << "SELECT query error: " << query.lastError().text();
    5 return;
    6 }
    7
    8 QSqlRecord record = query.record(); // 获取结果集元数据信息
    9 qDebug() << "Result set column count:" << record.count(); // 列数
    10 for (int i = 0; i < record.count(); ++i) {
    11 qDebug() << QString(" Column %1: Name = %2, Type = %3")
    12 .arg(i)
    13 .arg(record.fieldName(i)) // 列名
    14 .arg(record.field(i).type()); // 列类型
    15 }
    16
    17 while (query.next()) {
    18 // 遍历结果集...
    19 }

    这段代码获取了 employees 表查询结果集的元数据信息,输出了结果集的列数,并遍历输出了每列的名称和数据类型。

    总结

    通过 next() 函数遍历结果集,使用 value() 函数获取列值,可以方便地从 QSqlQuery 对象中获取 SELECT 查询的结果数据。QSqlQuery 还提供了其他函数,用于获取结果集的大小、元数据信息以及移动游标等操作。掌握这些函数的使用,可以灵活地处理各种查询结果。

    10.3.4 事务 (Transaction) 处理:begin(), commit(), rollback()

    事务 (Transaction) 是一系列数据库操作的逻辑单元,它保证了数据库操作的原子性 (Atomicity)、一致性 (Consistency)、隔离性 (Isolation) 和持久性 (Durability),即 ACID 特性。Qt SQL 模块通过 QSqlDatabase 类提供了事务处理的支持。

    事务的概念

    在数据库操作中,事务通常用于处理一组相关的操作,例如,在一个银行转账操作中,需要同时从一个账户扣款,并向另一个账户存款。这两个操作必须作为一个整体来执行,要么全部成功,要么全部失败。如果只成功执行了其中一个操作,就会导致数据不一致。

    事务可以保证一组数据库操作的原子性,即事务中的所有操作要么全部成功提交 (Commit),要么全部回滚 (Rollback)。如果事务执行过程中发生错误,或者应用程序显式地回滚事务,则事务中已经执行的所有操作都会被撤销,数据库状态会回退到事务开始之前的状态,保证了数据的一致性。

    事务的 ACID 特性

    原子性 (Atomicity):事务是一个不可分割的最小操作单元,事务中的所有操作要么全部成功,要么全部失败。
    一致性 (Consistency):事务执行前后,数据库的数据必须保持一致状态。例如,在转账操作中,转出账户的余额减少的金额,必须等于转入账户的余额增加的金额,总账户余额保持不变。
    隔离性 (Isolation):多个并发事务之间应该相互隔离,一个事务的执行不应该受到其他事务的干扰。数据库系统通常使用锁机制来实现事务的隔离性。隔离级别定义了事务之间的隔离程度。
    持久性 (Durability):一旦事务成功提交,其对数据库的修改就是永久性的,即使系统发生故障 (例如,断电、崩溃),已提交的事务数据也不会丢失。数据库系统通常使用事务日志 (Transaction Log) 来保证事务的持久性。

    Qt 中使用事务

    在 Qt SQL 模块中,可以使用 QSqlDatabase 类的 transaction(), commit(), rollback() 函数来控制事务的开始、提交和回滚。

    ① 开启事务:transaction() 函数

    QSqlDatabase::transaction() 函数用于开始一个新的事务。必须在执行任何数据库操作之前调用 transaction() 函数。transaction() 函数返回 true 如果成功开始事务,返回 false 如果开始事务失败 (例如,数据库连接无效)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::database(); // 获取默认数据库连接
    2 if (!db.transaction()) { // 开始事务
    3 qDebug() << "Start transaction error: " << db.lastError().text();
    4 return;
    5 }
    6 // 事务开始,可以执行一系列数据库操作...

    ② 提交事务:commit() 函数

    QSqlDatabase::commit() 函数用于提交当前事务。提交事务会将事务中所有已执行的数据库操作永久保存到数据库中。只有在事务成功提交后,事务中的修改才会生效。commit() 函数返回 true 如果成功提交事务,返回 false 如果提交事务失败。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 if (!db.commit()) { // 提交事务
    2 qDebug() << "Commit transaction error: " << db.lastError().text();
    3 db.rollback(); // 如果提交失败,通常需要回滚事务
    4 return;
    5 }
    6 qDebug() << "Transaction committed successfully!";

    ③ 回滚事务:rollback() 函数

    QSqlDatabase::rollback() 函数用于回滚当前事务。回滚事务会撤销事务中所有已执行的数据库操作,数据库状态会回退到事务开始之前的状态。通常在事务执行过程中发生错误,或者应用程序需要取消事务时,需要回滚事务。rollback() 函数返回 true 如果成功回滚事务,返回 false 如果回滚事务失败。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 if (!db.rollback()) { // 回滚事务
    2 qDebug() << "Rollback transaction error: " << db.lastError().text();
    3 return;
    4 }
    5 qDebug() << "Transaction rolled back successfully!";

    事务的使用模式

    典型的事务使用模式如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::database();
    2 if (!db.transaction()) { // 开始事务
    3 qDebug() << "Start transaction error: " << db.lastError().text();
    4 return;
    5 }
    6
    7 bool success = true;
    8 // 执行一系列数据库操作
    9 if (/* 操作 1 失败 */) {
    10 success = false;
    11 }
    12 if (/* 操作 2 失败 */) {
    13 success = false;
    14 }
    15 // ...
    16
    17 if (success) {
    18 if (!db.commit()) { // 提交事务
    19 qDebug() << "Commit transaction error: " << db.lastError().text();
    20 db.rollback(); // 如果提交失败,通常需要回滚事务
    21 } else {
    22 qDebug() << "Transaction committed successfully!";
    23 }
    24 } else {
    25 if (!db.rollback()) { // 回滚事务
    26 qDebug() << "Rollback transaction error: " << db.lastError().text();
    27 } else {
    28 qDebug() << "Transaction rolled back due to errors.";
    29 }
    30 }

    这段代码演示了一个典型的事务处理流程。首先开始事务,然后执行一系列数据库操作。如果在执行过程中发生任何错误,将 success 标志设置为 false。最后,根据 success 标志的值,决定提交事务还是回滚事务。如果在提交事务时也发生错误,通常也需要回滚事务,以保证数据一致性。

    事务嵌套

    某些数据库系统支持事务嵌套 (Nested Transactions),即在一个事务中开始另一个事务。Qt SQL 模块的事务处理是否支持嵌套事务,取决于底层的数据库驱动和数据库系统。SQLite 数据库默认不支持嵌套事务。MySQL 和 PostgreSQL 等数据库支持嵌套事务 (通过 savepoint 实现)。

    在 Qt SQL 模块中,即使底层数据库系统支持嵌套事务,QSqlDatabase::transaction(), commit(), rollback() 函数也只能控制最外层的事务。如果需要使用嵌套事务,可能需要使用数据库特定的 SQL 语法 (例如,MySQL 的 SAVEPOINT 语句)。

    总结

    事务处理是保证数据库数据一致性和完整性的重要机制。在 Qt 数据库编程中,可以使用 QSqlDatabase 类的 transaction(), commit(), rollback() 函数来控制事务的开始、提交和回滚。在处理一组相关的数据库操作时,务必使用事务,以保证操作的原子性和数据一致性。在事务处理过程中,要进行充分的错误处理,确保在事务执行失败或提交失败时,能够正确回滚事务,避免数据不一致。

    10.4 模型与数据库的集成

    本节将讲解如何使用 QSqlTableModelQSqlRelationalTableModel 类将数据库数据与 Qt 的视图 (View) 组件集成,实现数据库数据的可视化和编辑。模型/视图架构是 Qt 中用于数据展示和编辑的核心架构,通过模型/视图架构,可以实现数据与界面的分离,提高代码的可维护性和灵活性。

    10.4.1 QSqlTableModel 的使用:表格数据的模型

    QSqlTableModel 类提供了一个可编辑的 SQL 数据模型的实现,用于将数据库表的数据以表格形式展示在视图 (如 QTableView) 中,并允许用户编辑数据。QSqlTableModel 是 Qt 模型/视图架构中的模型 (Model) 组件。

    QSqlTableModel 的特点

    表格数据模型QSqlTableModel 将数据库表的数据视为一个表格数据模型,每一行代表表中的一条记录,每一列代表表中的一个字段。
    可编辑QSqlTableModel 允许用户在视图中直接编辑数据,模型的修改会自动同步到数据库中。
    基于 SQL 查询QSqlTableModel 的数据来源于一个 SQL SELECT 查询。可以通过设置表名、过滤条件、排序方式等来控制模型的数据内容。
    与视图集成QSqlTableModel 可以与 QTableView, QListView, QTreeView 等视图组件配合使用,将数据库数据展示在用户界面上。

    QSqlTableModel 的使用步骤

    创建 QSqlTableModel 对象:创建一个 QSqlTableModel 对象,并指定父对象 (通常是窗口或对话框)。可以指定一个数据库连接名称作为参数,如果不指定,则使用默认数据库连接。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlTableModel *model = new QSqlTableModel(this); // 使用默认数据库连接
    2 QSqlTableModel *model = new QSqlTableModel(this, QSqlDatabase::database("myConnection")); // 使用指定的数据库连接

    设置表名:使用 setTable(const QString &tableName) 函数设置要操作的数据库表名。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 model->setTable("employees"); // 设置表名为 "employees"

    设置选择模式 (可选):可以使用 setEditStrategy(QSqlTableModel::EditStrategy strategy) 函数设置数据编辑策略。常用的策略包括:

    QSqlTableModel::OnManualSubmit (默认):手动提交模式。在视图中修改数据后,需要显式调用 submitAll() 函数才能将修改提交到数据库。
    QSqlTableModel::OnRowChange:行更改提交模式。在视图中完成一行数据的修改后,修改会自动提交到数据库。
    QSqlTableModel::OnFieldChange:字段更改提交模式。在视图中完成一个字段数据的修改后,修改会自动提交到数据库。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 model->setEditStrategy(QSqlTableModel::OnManualSubmit); // 设置为手动提交模式

    设置过滤条件 (可选):可以使用 setFilter(const QString &filter) 函数设置 SQL WHERE 子句的过滤条件,限制模型中显示的数据行。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 model->setFilter("age > 30"); // 只显示年龄大于 30 的员工记录

    设置排序方式 (可选):可以使用 setSort(int column, Qt::SortOrder order) 函数设置排序方式。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 model->setSort(1, Qt::AscendingOrder); // 按照第 2 列 (索引为 1,通常是 name 列) 升序排序

    选择数据:select() 函数:调用 select() 函数执行 SQL SELECT 查询,从数据库中获取数据并填充模型。select() 函数返回 true 如果查询成功,返回 false 如果查询失败。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 if (!model->select()) { // 执行 SELECT 查询
    2 qDebug() << "Select table data error: " << model->lastError().text();
    3 return;
    4 }
    5 qDebug() << "Table data selected successfully!";

    设置表头 (可选):可以使用 setHeaderData(int section, Qt::Orientation orientation, const QVariant &value, int role = Qt::DisplayRole) 函数设置表头显示文本。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 model->setHeaderData(0, Qt::Horizontal, QObject::tr("ID")); // 设置第 1 列表头为 "ID"
    2 model->setHeaderData(1, Qt::Horizontal, QObject::tr("Name")); // 设置第 2 列表头为 "Name"
    3 model->setHeaderData(2, Qt::Horizontal, QObject::tr("Age")); // 设置第 3 列表头为 "Age"
    4 // ...

    将模型与视图关联:创建 QTableView 视图组件,并使用 setModel(QAbstractItemModel *model) 函数将 QSqlTableModel 对象设置为视图的数据模型。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTableView *tableView = new QTableView(this);
    2 tableView->setModel(model); // 将模型与视图关联

    显示视图:将 QTableView 视图组件添加到窗口或对话框中,并显示出来。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QVBoxLayout *layout = new QVBoxLayout;
    2 layout->addWidget(tableView);
    3 setLayout(layout);

    示例代码 (使用 QSqlTableModel 显示 employees 表数据)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QTableView>
    3 #include <QVBoxLayout>
    4 #include <QSqlDatabase>
    5 #include <QSqlError>
    6 #include <QSqlTableModel>
    7 #include <QDebug>
    8 #include <QHeaderView>
    9
    10 class MainWindow : public QWidget {
    11 Q_OBJECT
    12
    13 public:
    14 MainWindow(QWidget *parent = nullptr) : QWidget(parent) {
    15 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    16 db.setDatabaseName("mydatabase.db");
    17 if (!db.open()) {
    18 qDebug() << "Database connection error: " << db.lastError().text();
    19 return;
    20 }
    21
    22 QSqlTableModel *model = new QSqlTableModel(this);
    23 model->setTable("employees");
    24 model->setEditStrategy(QSqlTableModel::OnManualSubmit);
    25
    26 if (!model->select()) {
    27 qDebug() << "Select table data error: " << model->lastError().text();
    28 return;
    29 }
    30
    31 model->setHeaderData(0, Qt::Horizontal, QObject::tr("ID"));
    32 model->setHeaderData(1, Qt::Horizontal, QObject::tr("Name"));
    33 model->setHeaderData(2, Qt::Horizontal, QObject::tr("Age"));
    34 model->setHeaderData(3, Qt::Horizontal, QObject::tr("Department"));
    35
    36 QTableView *tableView = new QTableView(this);
    37 tableView->setModel(model);
    38 tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch); // 水平表头自动伸缩
    39
    40 QVBoxLayout *layout = new QVBoxLayout;
    41 layout->addWidget(tableView);
    42 setLayout(layout);
    43 }
    44
    45 private:
    46 QSqlTableModel *model;
    47 };

    这段代码创建了一个 MainWindow 窗口,使用 QSqlTableModel 加载 employees 表的数据,并通过 QTableView 视图显示出来。用户可以在视图中浏览和编辑数据 (根据 setEditStrategy() 设置的编辑策略)。

    10.4.2 QSqlRelationalTableModel 的使用:关联表格的模型

    QSqlRelationalTableModel 类继承自 QSqlTableModel,扩展了对关联表 (Relational Table) 的支持。它特别适用于处理包含外键 (Foreign Key) 关系的数据库表。QSqlRelationalTableModel 可以自动处理外键关系,在视图中以友好的方式展示关联表的数据,并提供方便的外键编辑功能。

    QSqlRelationalTableModel 的特点

    关联表支持QSqlRelationalTableModel 能够处理包含外键关系的数据库表,自动加载和展示关联表的数据。
    外键关联显示:可以将外键字段显示为关联表中对应行的某个字段值,而不是原始的外键 ID,提高用户可读性。
    外键关联编辑:在编辑包含外键的数据时,可以提供下拉列表等控件,方便用户从关联表中选择外键值。
    继承自 QSqlTableModelQSqlRelationalTableModel 继承了 QSqlTableModel 的所有功能,包括表格数据模型、可编辑性、基于 SQL 查询等特性。

    QSqlRelationalTableModel 的使用步骤

    创建 QSqlRelationalTableModel 对象:类似于 QSqlTableModel,创建 QSqlRelationalTableModel 对象,并指定父对象和数据库连接 (可选)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlRelationalTableModel *relModel = new QSqlRelationalTableModel(this);

    设置主表名:使用 setTable(const QString &tableName) 函数设置主表名 (即包含外键的表)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 relModel->setTable("employees"); // 主表为 "employees"

    设置编辑策略 (可选):使用 setEditStrategy() 函数设置编辑策略,与 QSqlTableModel 相同。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 relModel->setEditStrategy(QSqlRelationalTableModel::OnManualSubmit);

    设置外键关联关系:setRelation() 函数:使用 setRelation(int column, const QSqlRelation &relation) 函数为指定的列设置外键关联关系。

    column:外键列的索引 (从 0 开始)。
    relationQSqlRelation 对象,描述了外键列与关联表之间的关系。QSqlRelation 对象需要指定关联表名、关联表的 ID 列名 (通常是主键列) 和关联表的显示列名 (用于在视图中显示)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlRelation departmentRelation("departments", "id", "name"); // 关联表 "departments", ID 列 "id", 显示列 "name"
    2 relModel->setRelation(3, departmentRelation); // 第 4 列 (索引为 3,假设是 department_id 列) 设置外键关联

    这段代码创建了一个 QSqlRelation 对象,描述了 employees 表的 department_id 列与 departments 表之间的外键关系。departments 表的主键列是 id,要显示的列是 name。然后使用 setRelation() 函数将这个关联关系设置到 QSqlRelationalTableModel 对象的第 4 列 (索引为 3) 上。

    选择数据:select() 函数:调用 select() 函数执行 SQL SELECT 查询,从数据库中获取数据并填充模型。与 QSqlTableModel 相同。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 if (!relModel->select()) {
    2 qDebug() << "Select relational table data error: " << relModel->lastError().text();
    3 return;
    4 }
    5 qDebug() << "Relational table data selected successfully!";

    设置表头 (可选):使用 setHeaderData() 函数设置表头显示文本,与 QSqlTableModel 相同。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 relModel->setHeaderData(0, Qt::Horizontal, QObject::tr("ID"));
    2 relModel->setHeaderData(1, Qt::Horizontal, QObject::tr("Name"));
    3 relModel->setHeaderData(2, Qt::Horizontal, QObject::tr("Age"));
    4 relModel->setHeaderData(3, Qt::Horizontal, QObject::tr("Department")); // 表头显示 "Department" 而不是 "department_id"
    5 // ...

    将模型与视图关联:创建 QTableView 视图组件,并使用 setModel() 函数将 QSqlRelationalTableModel 对象设置为视图的数据模型。与 QSqlTableModel 相同。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QTableView *tableView = new QTableView(this);
    2 tableView->setModel(relModel);

    设置委托 (Delegate) (可选):对于外键列,QSqlRelationalTableModel 默认会使用 QSqlRelationalDelegate 委托。QSqlRelationalDelegate 会为外键列的单元格提供下拉列表控件,显示关联表中的显示列数据,方便用户选择外键值。如果需要自定义外键列的显示和编辑方式,可以创建自定义的委托 (Delegate) 并设置给视图。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 如果需要自定义委托,可以创建自定义的委托类,例如 MyRelationDelegate
    2 // tableView->setItemDelegateForColumn(3, new MyRelationDelegate(this)); // 为第 4 列设置自定义委托

    显示视图:将 QTableView 视图组件添加到窗口或对话框中,并显示出来。与 QSqlTableModel 相同。

    示例代码 (使用 QSqlRelationalTableModel 显示 employees 表和 departments 表关联数据)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QTableView>
    3 #include <QVBoxLayout>
    4 #include <QSqlDatabase>
    5 #include <QSqlError>
    6 #include <QSqlRelationalTableModel>
    7 #include <QSqlRelation>
    8 #include <QDebug>
    9 #include <QHeaderView>
    10
    11 class MainWindow : public QWidget {
    12 Q_OBJECT
    13
    14 public:
    15 MainWindow(QWidget *parent = nullptr) : QWidget(parent) {
    16 QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
    17 db.setDatabaseName("mydatabase.db");
    18 if (!db.open()) {
    19 qDebug() << "Database connection error: " << db.lastError().text();
    20 return;
    21 }
    22
    23 QSqlRelationalTableModel *relModel = new QSqlRelationalTableModel(this);
    24 relModel->setTable("employees");
    25 relModel->setEditStrategy(QSqlRelationalTableModel::OnManualSubmit);
    26
    27 QSqlRelation departmentRelation("departments", "id", "name");
    28 relModel->setRelation(3, departmentRelation); // 第 4 列 (department_id) 关联 departments 表
    29
    30 if (!relModel->select()) {
    31 qDebug() << "Select relational table data error: " << relModel->lastError().text();
    32 return;
    33 }
    34
    35 relModel->setHeaderData(0, Qt::Horizontal, QObject::tr("ID"));
    36 relModel->setHeaderData(1, Qt::Horizontal, QObject::tr("Name"));
    37 relModel->setHeaderData(2, Qt::Horizontal, QObject::tr("Age"));
    38 relModel->setHeaderData(3, Qt::Horizontal, QObject::tr("Department")); // 表头显示 "Department"
    39
    40 QTableView *tableView = new QTableView(this);
    41 tableView->setModel(relModel);
    42 tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
    43
    44 QVBoxLayout *layout = new QVBoxLayout;
    45 layout->addWidget(tableView);
    46 setLayout(layout);
    47 }
    48
    49 private:
    50 QSqlRelationalTableModel *relModel;
    51 };

    这段代码与 10.4.1 节的示例代码类似,但使用了 QSqlRelationalTableModel 类,并设置了外键关联关系。在视图中,employees 表的 department_id 列将显示为 departments 表的 name 列数据,并且在编辑时会提供下拉列表,方便用户选择部门。

    10.4.3 数据的提交与回滚:submitAll(), revertAll()

    当使用 QSqlTableModelQSqlRelationalTableModel 并设置编辑策略为 QSqlTableModel::OnManualSubmit (手动提交模式) 时,在视图中对数据进行的修改不会立即同步到数据库中,而是先缓存在模型中。需要显式调用 submitAll() 函数才能将修改提交到数据库,或者使用 revertAll() 函数回滚所有未提交的修改。

    数据提交:submitAll() 函数

    QSqlTableModel::submitAll() 函数用于将模型中所有未提交的修改 (包括插入、更新和删除操作) 提交到数据库。submitAll() 函数返回 true 如果所有修改都成功提交,返回 false 如果提交过程中发生错误 (例如,违反数据库约束、连接中断等)。如果提交失败,可以使用 lastError() 函数获取错误信息。

    示例代码 (数据提交)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlTableModel *model = new QSqlTableModel(this);
    2 model->setTable("employees");
    3 model->setEditStrategy(QSqlTableModel::OnManualSubmit);
    4 model->select();
    5
    6 // 在视图中对数据进行修改...
    7
    8 if (model->submitAll()) { // 提交所有修改
    9 qDebug() << "Data changes submitted to database successfully!";
    10 } else {
    11 qDebug() << "Failed to submit data changes: " << model->lastError().text();
    12 model->revertAll(); // 如果提交失败,通常需要回滚修改
    13 }

    数据回滚:revertAll() 函数

    QSqlTableModel::revertAll() 函数用于回滚模型中所有未提交的修改,撤销所有在视图中进行的编辑操作,将模型数据恢复到上次提交或加载时的状态。revertAll() 函数总是返回 true (即使没有未提交的修改)。

    示例代码 (数据回滚)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlTableModel *model = new QSqlTableModel(this);
    2 model->setTable("employees");
    3 model->setEditStrategy(QSqlTableModel::OnManualSubmit);
    4 model->select();
    5
    6 // 在视图中对数据进行修改,但不提交...
    7
    8 model->revertAll(); // 回滚所有修改
    9 qDebug() << "Data changes reverted.";

    何时提交或回滚数据

    在手动提交模式下,何时调用 submitAll()revertAll() 函数,取决于应用程序的业务逻辑和用户交互需求。

    提交数据:通常在用户完成数据编辑操作,并确认保存修改时,调用 submitAll() 函数提交数据。例如,在用户点击 "保存" 按钮时提交修改。
    回滚数据:在用户取消编辑操作,或发生错误需要撤销修改时,调用 revertAll() 函数回滚数据。例如,在用户点击 "取消" 按钮或关闭编辑窗口时回滚修改。

    事务处理与数据提交回滚

    在需要保证数据一致性的场景下,可以将 submitAll()revertAll() 操作放在事务中进行。例如,在提交数据前开始一个事务,如果 submitAll() 函数返回 true,则提交事务;如果 submitAll() 函数返回 false,则回滚事务。这样可以保证数据提交的原子性,要么所有修改都成功提交,要么全部回滚。

    示例代码 (在事务中提交数据)

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSqlDatabase db = QSqlDatabase::database();
    2 QSqlTableModel *model = new QSqlTableModel(this, db);
    3 model->setTable("employees");
    4 model->setEditStrategy(QSqlTableModel::OnManualSubmit);
    5 model->select();
    6
    7 // 在视图中对数据进行修改...
    8
    9 if (db.transaction()) { // 开始事务
    10 if (model->submitAll()) { // 提交数据
    11 if (db.commit()) { // 提交事务
    12 qDebug() << "Data changes submitted and transaction committed successfully!";
    13 } else {
    14 qDebug() << "Commit transaction error after data submit: " << db.lastError().text();
    15 db.rollback(); // 如果事务提交失败,需要回滚事务
    16 model->revertAll(); // 同时回滚模型数据
    17 }
    18 } else {
    19 qDebug() << "Failed to submit data changes: " << model->lastError().text();
    20 db.rollback(); // 如果数据提交失败,需要回滚事务
    21 model->revertAll(); // 同时回滚模型数据
    22 }
    23 } else {
    24 qDebug() << "Start transaction error before data submit: " << db.lastError().text();
    25 model->revertAll(); // 如果事务开始失败,也需要回滚模型数据
    26 }

    这段代码演示了如何在事务中进行数据提交。首先开始事务,然后尝试提交模型数据。如果数据提交成功,则提交事务;如果数据提交失败或事务提交失败,则回滚事务,并回滚模型数据,保证数据的一致性。

    总结

    submitAll()revertAll() 函数提供了对 QSqlTableModelQSqlRelationalTableModel 模型数据的提交和回滚操作。在手动提交模式下,需要显式调用这两个函数来同步模型数据与数据库数据。在需要保证数据一致性的场景下,建议将数据提交和回滚操作放在事务中进行,以确保数据操作的原子性和一致性。

    11. Qt Quick 与 QML:现代 UI 开发

    11.1 Qt Quick 与 QML 概述

    11.1.1 Qt Quick 的特点:声明式 UI (Declarative UI), 动画效果 (Animation Effects), 硬件加速 (Hardware Acceleration)

    Qt Quick 是一套用于构建现代用户界面 (UI) 的技术,它强调 声明式 UI (Declarative UI)动画效果 (Animation Effects)硬件加速 (Hardware Acceleration)。与传统的基于 QWidget 的方法相比,Qt Quick 旨在提供更流畅、更具吸引力的用户体验,尤其适用于现代触摸屏设备和嵌入式系统。

    声明式 UI (Declarative UI)
    Qt Quick 使用 QML (Qt Meta-Object Language) 语言来描述用户界面。QML 是一种声明式语言,这意味着你只需描述界面的外观和行为,而无需编写复杂的命令式代码来一步步构建 UI。声明式 UI 的主要优势在于:
    ▮▮▮▮ⓐ 简洁性 (Conciseness):QML 代码通常比命令式代码更简洁、更易于阅读和维护。
    ▮▮▮▮ⓑ 直观性 (Intuitiveness):QML 的语法更贴近设计师的思维模式,更容易理解 UI 的结构和组件之间的关系。
    ▮▮▮▮ⓒ 高效性 (Efficiency):声明式描述允许 Qt Quick 引擎进行优化,例如延迟加载和增量更新,从而提高性能。

    例如,在 QML 中创建一个矩形和一个文本标签非常简单:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 import QtQuick
    2
    3 Rectangle {
    4 width: 200
    5 height: 100
    6 color: "lightblue"
    7
    8 Text {
    9 anchors.centerIn: parent
    10 text: "Hello, Qt Quick!"
    11 font.pixelSize: 20
    12 }
    13 }

    这段代码声明了一个浅蓝色的矩形,并在其中居中放置了一个文本 "Hello, Qt Quick!"。整个 UI 的结构和属性都清晰地声明在代码中。

    动画效果 (Animation Effects)
    Qt Quick 内置了强大的动画支持,使得创建流畅、自然的动画效果变得非常容易。动画在现代 UI 中至关重要,它们可以提升用户体验,使用户界面更具吸引力和互动性。Qt Quick 提供了多种动画类型,例如:
    ▮▮▮▮ⓐ 属性动画 (Property Animation):平滑地改变元素属性的值,例如位置、大小、颜色等。
    ▮▮▮▮ⓑ 过渡效果 (Transitions):在状态切换时自动应用动画效果。
    ▮▮▮▮ⓒ 粒子效果 (Particle Effects):创建复杂的视觉效果,例如火焰、烟雾、星尘等。

    例如,让一个矩形在点击时平滑移动到新的位置:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 import QtQuick
    2
    3 Rectangle {
    4 id: rect
    5 width: 100
    6 height: 100
    7 color: "red"
    8 x: 50
    9 y: 50
    10
    11 MouseArea {
    12 anchors.fill: parent
    13 onClicked: {
    14 rect.x = Math.random() * (parent.width - rect.width)
    15 rect.y = Math.random() * (parent.height - rect.height)
    16 moveAnimation.start() // 启动动画
    17 }
    18 }
    19
    20 PropertyAnimation {
    21 id: moveAnimation
    22 target: rect
    23 properties: "x,y"
    24 duration: 500
    25 easing.type: Easing.InOutQuad
    26 }
    27 }

    这段代码中,PropertyAnimation 定义了一个平滑移动矩形的动画。当鼠标点击矩形时,矩形会动画地移动到一个随机的新位置。

    硬件加速 (Hardware Acceleration)
    Qt Quick 充分利用 GPU (图形处理器) 的硬件加速能力来渲染用户界面。硬件加速可以显著提高图形渲染的性能,尤其是在处理复杂动画和视觉效果时。硬件加速带来的优势包括:
    ▮▮▮▮ⓐ 流畅的帧率 (Smooth Frame Rate):即使在复杂的 UI 场景中,也能保持高帧率,提供流畅的用户体验。
    ▮▮▮▮ⓑ 降低 CPU 负载 (Reduced CPU Load):将图形渲染任务转移到 GPU,可以减轻 CPU 的负担,使 CPU 能够更专注于其他任务。
    ▮▮▮▮ⓒ 丰富的视觉效果 (Rich Visual Effects):硬件加速使得实现更复杂、更精美的视觉效果成为可能,而不会牺牲性能。

    Qt Quick 的硬件加速默认是启用的,通常情况下开发者无需额外配置。Qt Quick 引擎会自动检测可用的 GPU,并利用 GPU 进行渲染。这使得 Qt Quick 应用在各种硬件平台上都能获得良好的性能表现,尤其是在嵌入式设备和移动设备上。

    总而言之,Qt Quick 通过声明式 UI、强大的动画效果和硬件加速,为开发者提供了一个构建现代、高性能用户界面的强大工具集。它特别适合开发需要精美视觉效果和流畅用户体验的应用,例如移动应用、嵌入式系统界面和现代桌面应用。

    11.1.2 QML 语言:声明式语法 (Declarative Syntax), JavaScript 集成 (JavaScript Integration)

    QML (Qt Meta-Object Language) 是 Qt Quick 框架的核心语言,用于描述用户界面的结构和行为。QML 是一种 声明式语言 (Declarative Language),它专注于描述 "是什么 (what)" 而不是 "怎么做 (how)"。此外,QML 还紧密集成了 JavaScript,用于处理逻辑和动态行为。

    声明式语法 (Declarative Syntax)
    QML 的语法设计目标是简洁、直观且易于理解,尤其对于设计师和前端开发者来说更加友好。QML 代码主要由以下几个核心概念组成:
    ▮▮▮▮ⓐ 元素 (Elements):QML 的基本构建块是元素,例如 RectangleTextImageButton 等。元素代表了 UI 中的可视组件或逻辑组件。
    ▮▮▮▮ⓑ 属性 (Properties):每个元素都有一组属性,用于描述元素的外观、位置、大小、行为等。例如,Rectangle 元素有 widthheightcolor 等属性,Text 元素有 textfontcolor 等属性。
    ▮▮▮▮ⓒ 信号 (Signals) 与槽 (Slots):QML 继承了 Qt 的 信号与槽 (Signals and Slots) 机制,用于处理事件和组件之间的通信。信号表示事件的发生,槽是响应信号的函数。
    ▮▮▮▮ⓓ 绑定 (Bindings):QML 允许使用属性绑定,将一个属性的值动态地关联到另一个属性或表达式。当被绑定的属性发生变化时,绑定目标属性会自动更新。

    QML 的声明式语法使得 UI 代码更易于阅读和维护。例如,以下 QML 代码描述了一个简单的按钮:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 import QtQuick
    2
    3 Rectangle {
    4 width: 120
    5 height: 40
    6 color: "lightgray"
    7 border.color: "gray"
    8 border.width: 1
    9
    10 Text {
    11 anchors.centerIn: parent
    12 text: "Click Me"
    13 font.pixelSize: 16
    14 }
    15
    16 MouseArea {
    17 anchors.fill: parent
    18 onClicked: {
    19 console.log("Button Clicked!")
    20 }
    21 }
    22 }

    这段代码声明了一个矩形作为按钮的背景,一个文本显示按钮的文字,以及一个 MouseArea 处理鼠标点击事件。整个按钮的结构和行为都声明式地定义在代码中。

    JavaScript 集成 (JavaScript Integration)
    QML 与 JavaScript 深度集成,JavaScript 可以嵌入到 QML 代码中,用于处理更复杂的逻辑、数据操作和动态行为。JavaScript 在 QML 中扮演着重要的角色:
    ▮▮▮▮ⓐ 事件处理 (Event Handling):虽然信号与槽机制用于组件间的通信,但 JavaScript 常常用于编写事件处理函数,响应用户的交互操作。例如,MouseAreaonClicked 信号可以使用 JavaScript 函数来处理点击事件。
    ▮▮▮▮ⓑ 数据处理 (Data Processing):JavaScript 可以用于处理数据,例如格式化字符串、计算数值、操作数组和对象等。
    ▮▮▮▮ⓒ 动态行为 (Dynamic Behavior):JavaScript 可以根据条件动态地修改 QML 元素的属性,实现更灵活的 UI 行为。
    ▮▮▮▮ⓓ 与 C++ 后端交互 (Interaction with C++ Backend):在 Qt Quick 应用中,QML 前端通常与 C++ 后端协同工作。JavaScript 可以调用 C++ 暴露的方法和属性,实现前端与后端的交互。

    例如,使用 JavaScript 实现一个简单的计数器功能:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 import QtQuick
    2
    3 Rectangle {
    4 width: 200
    5 height: 100
    6 color: "white"
    7
    8 property int count: 0 // 定义一个属性 count
    9
    10 Text {
    11 id: counterText
    12 anchors.centerIn: parent
    13 text: "Count: " + count
    14 font.pixelSize: 24
    15 }
    16
    17 MouseArea {
    18 anchors.fill: parent
    19 onClicked: {
    20 count++ // JavaScript 代码递增 count 属性
    21 console.log("Count incremented to: " + count)
    22 }
    23 }
    24 }

    在这个例子中,count 属性使用 JavaScript 代码在每次点击时递增,counterTexttext 属性通过属性绑定动态地显示 count 的值。JavaScript 的集成使得 QML 能够处理动态逻辑和用户交互。

    总结来说,QML 语言以其声明式语法和 JavaScript 集成特性,为开发者提供了一种高效、灵活的方式来构建现代用户界面。声明式语法使得 UI 结构清晰易懂,而 JavaScript 的集成则提供了强大的逻辑处理能力,使得 QML 成为构建复杂、动态 UI 的理想选择。

    11.1.3 Qt Quick 应用的架构:QML 前端 (QML Frontend) 与 C++ 后端 (C++ Backend)

    在典型的 Qt Quick 应用架构中,通常采用 QML 前端 (QML Frontend)C++ 后端 (C++ Backend) 分离的模式。这种架构模式充分利用了 QML 和 C++ 各自的优势,实现了 UI 设计与业务逻辑的良好分离,提高了开发效率和代码可维护性。

    QML 前端 (QML Frontend)
    QML 前端负责用户界面的 呈现 (Presentation)用户交互 (User Interaction)。其主要职责包括:
    ▮▮▮▮ⓐ UI 结构定义 (UI Structure Definition):使用 QML 语言声明用户界面的结构,包括各种可视元素(如窗口、按钮、文本框、图像等)和布局。
    ▮▮▮▮ⓑ 视觉效果与动画 (Visual Effects and Animations):利用 Qt Quick 强大的动画和视觉效果功能,创建现代、流畅的用户体验。
    ▮▮▮▮ⓒ 用户交互处理 (User Interaction Handling):处理用户的输入事件,例如鼠标点击、键盘输入、触摸手势等。通常使用 JavaScript 代码来响应用户交互,并触发相应的 UI 行为或调用后端功能。
    ▮▮▮▮ⓓ 数据展示 (Data Presentation):从 C++ 后端获取数据,并在 UI 上展示。QML 可以通过属性绑定、模型/视图 (Model/View) 架构等方式高效地展示数据。

    QML 前端开发的优势在于:
    ▮▮▮▮ⓐ 快速迭代 (Rapid Iteration):QML 代码通常易于编写和修改,可以快速迭代 UI 设计,方便进行原型开发和界面调整。
    ▮▮▮▮ⓑ 设计师友好 (Designer-Friendly):QML 的声明式语法和可视化工具(如 Qt Design Studio)使得设计师可以更容易地参与到 UI 开发过程中。
    ▮▮▮▮ⓒ 热重载 (Hot Reloading):在开发过程中,修改 QML 代码后通常可以立即在运行的应用中看到效果,无需重新编译和启动,提高了开发效率。

    C++ 后端 (C++ Backend)
    C++ 后端负责应用的 业务逻辑 (Business Logic)数据处理 (Data Processing)系统集成 (System Integration)。其主要职责包括:
    ▮▮▮▮ⓐ 核心业务逻辑 (Core Business Logic):实现应用的核心功能和业务规则,例如数据计算、算法处理、业务流程控制等。
    ▮▮▮▮ⓑ 数据模型 (Data Model):管理应用的数据,包括数据存储、数据访问、数据更新等。C++ 可以高效地处理复杂的数据结构和数据操作。
    ▮▮▮▮ⓒ 系统接口 (System Interfaces):与操作系统、硬件设备、网络服务、数据库等进行交互。C++ 提供了丰富的库和接口,可以方便地进行系统级编程。
    ▮▮▮▮ⓓ 性能关键型任务 (Performance-Critical Tasks):对于性能要求高的任务,例如图像处理、音视频编解码、密集计算等,C++ 具有更高的执行效率。

    C++ 后端开发的优势在于:
    ▮▮▮▮ⓐ 高性能 (High Performance):C++ 是一种编译型语言,具有卓越的性能,适用于处理计算密集型任务和对响应速度要求高的场景。
    ▮▮▮▮ⓑ 强大的系统编程能力 (Powerful System Programming Capabilities):C++ 提供了丰富的系统编程接口和底层控制能力,可以方便地进行系统级开发和硬件交互。
    ▮▮▮▮ⓒ 成熟的生态系统 (Mature Ecosystem):C++ 拥有庞大而成熟的生态系统,包括丰富的库、框架和工具,可以支持各种复杂的应用开发需求。
    ▮▮▮▮ⓓ 代码复用 (Code Reusability):C++ 代码具有良好的可复用性,可以将核心业务逻辑封装成库,供多个应用或模块共享使用。

    QML 前端与 C++ 后端的交互
    QML 前端和 C++ 后端通过 Qt 的元对象系统 (Meta-Object System) 进行高效的交互。Qt 提供了多种机制来实现 QML 与 C++ 之间的通信和数据传递:
    ▮▮▮▮ⓐ 属性暴露 (Property Exposure):C++ 类可以将属性暴露给 QML,QML 可以像访问普通属性一样读取和修改 C++ 对象的属性。
    ▮▮▮▮ⓑ 信号与槽连接 (Signal and Slot Connection):QML 可以连接到 C++ 对象的信号,并在信号发射时执行 JavaScript 代码或调用 QML 函数。C++ 也可以连接到 QML 对象的信号,响应 QML 前端的事件。
    ▮▮▮▮ⓒ 方法调用 (Method Invocation):QML 可以调用 C++ 对象的方法,执行 C++ 后端的功能。C++ 也可以通过 QML 对象的接口调用 QML 函数。
    ▮▮▮▮ⓓ 上下文属性 (Context Properties) 和上下文对象 (Context Objects):Qt 允许将 C++ 对象注册为 QML 的上下文属性或上下文对象,使得 QML 代码可以直接访问这些 C++ 对象及其属性和方法。

    通过这些交互机制,QML 前端和 C++ 后端可以协同工作,共同构建功能完善、性能优良的 Qt Quick 应用。QML 负责 UI 呈现和交互逻辑,C++ 负责核心业务逻辑和数据处理,两者分工明确,优势互补。

    总而言之,Qt Quick 应用的 QML 前端与 C++ 后端分离架构是一种高效、灵活的开发模式。它使得 UI 设计与业务逻辑解耦,提高了开发效率、代码可维护性和团队协作效率。这种架构模式特别适用于构建现代、复杂的用户界面应用,例如桌面应用、移动应用和嵌入式系统界面。

    11.2 QML 基础语法

    11.2.1 QML 元素 (Element):Rectangle, Text, Image, MouseArea

    QML 元素 (Elements) 是构建 QML 用户界面的基本 building blocks。它们类似于 HTML 中的标签或 QWidget 中的控件,代表了 UI 中的可视组件或逻辑组件。Qt Quick 提供了丰富的内置元素,涵盖了各种常见的 UI 组件和功能。以下介绍几种常用的 QML 元素:

    Rectangle 元素
    Rectangle 元素用于绘制矩形区域,可以作为 UI 的背景、容器或简单的图形元素。Rectangle 元素的主要属性包括:
    ▮▮▮▮ⓐ widthheight:矩形的宽度和高度,可以使用像素值或相对值。
    ▮▮▮▮ⓑ color:矩形的填充颜色,可以使用颜色名称、十六进制颜色码或 Qt.rgba() 等函数指定。
    ▮▮▮▮ⓒ border.colorborder.width:矩形的边框颜色和宽度。
    ▮▮▮▮ⓓ radius:矩形的圆角半径,可以创建圆角矩形。
    ▮▮▮▮ⓔ gradient:矩形的渐变填充效果,可以使用 Gradient 类型定义渐变。

    例如,创建一个蓝色背景的圆角矩形:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 200
    3 height: 100
    4 color: "lightblue"
    5 border.color: "darkblue"
    6 border.width: 2
    7 radius: 10
    8 }

    Text 元素
    Text 元素用于显示文本内容。Text 元素的主要属性包括:
    ▮▮▮▮ⓐ text:要显示的文本字符串。
    ▮▮▮▮ⓑ font.familyfont.pixelSizefont.bold 等:文本的字体属性,可以设置字体族、像素大小、粗体等。
    ▮▮▮▮ⓒ color:文本的颜色。
    ▮▮▮▮ⓓ horizontalAlignmentverticalAlignment:文本的水平和垂直对齐方式。
    ▮▮▮▮ⓔ elide:文本过长时的省略方式,例如 Text.ElideRight 表示在右侧省略。

    例如,显示一段红色、粗体的文本:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Text {
    2 text: "This is a sample text."
    3 font.pixelSize: 20
    4 font.bold: true
    5 color: "red"
    6 }

    Image 元素
    Image 元素用于显示图像。Image 元素的主要属性包括:
    ▮▮▮▮ⓐ source:图像文件的路径,可以使用本地文件路径或网络 URL。
    ▮▮▮▮ⓑ widthheight:图像的宽度和高度,可以缩放图像。
    ▮▮▮▮ⓒ fillMode:图像的填充模式,例如 Image.PreserveAspectFit 表示保持宽高比缩放以适应元素大小。
    ▮▮▮▮ⓓ asynchronous:是否异步加载图像,设置为 true 可以提高 UI 响应性。

    例如,显示一张本地图片:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Image {
    2 source: "images/logo.png" // 假设 images 目录下有 logo.png 文件
    3 width: 150
    4 height: 100
    5 fillMode: Image.PreserveAspectFit
    6 }

    MouseArea 元素
    MouseArea 元素用于接收鼠标和触摸事件。MouseArea 本身不可见,通常作为交互区域附加到其他可视元素上。MouseArea 主要的信号包括:
    ▮▮▮▮ⓐ onClicked:鼠标点击事件。
    ▮▮▮▮ⓑ onPressedonReleased:鼠标按下和释放事件。
    ▮▮▮▮ⓒ onEnteredonExited:鼠标进入和离开事件。
    ▮▮▮▮ⓓ onPositionChanged:鼠标位置改变事件(鼠标移动)。

    例如,创建一个可点击的矩形区域,点击时输出日志:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 100
    3 height: 50
    4 color: "lightgreen"
    5
    6 MouseArea {
    7 anchors.fill: parent
    8 onClicked: {
    9 console.log("Rectangle Clicked!")
    10 }
    11 }
    12 }

    除了以上介绍的 RectangleTextImageMouseArea 元素,Qt Quick 还提供了大量的其他内置元素,用于构建各种 UI 组件和功能,例如:

    基本元素 (Basic Elements)Item (基本容器)、Component (组件定义)、Loader (动态加载组件) 等。
    容器元素 (Container Elements)RowColumnGridFlow (布局容器)、StackView (堆叠视图)、TabView (标签页视图) 等。
    控件元素 (Control Elements)Button (按钮)、CheckBox (复选框)、RadioButton (单选按钮)、Slider (滑块)、TextInput (文本输入框)、ComboBox (下拉框)、ListView (列表视图)、TableView (表格视图)、TreeView (树形视图) 等。
    媒体元素 (Media Elements)Video (视频播放)、Audio (音频播放)、Camera (摄像头) 等。
    图形效果元素 (Graphical Effect Elements)DropShadow (阴影效果)、GaussianBlur (高斯模糊)、Colorize (颜色化) 等。
    动画元素 (Animation Elements)PropertyAnimation (属性动画)、NumberAnimation (数值动画)、ColorAnimation (颜色动画)、SequentialAnimation (序列动画)、ParallelAnimation (并行动画)、Transitions (过渡效果) 等。
    输入元素 (Input Elements)MouseArea (鼠标区域)、KeyboardArea (键盘区域)、Flickable (可 Flick 手势区域)、PinchArea (捏合手势区域) 等。

    开发者可以根据应用需求选择合适的 QML 元素,组合构建出丰富的用户界面。此外,QML 还支持 自定义元素 (Custom Elements),允许开发者将一组 QML 元素和逻辑封装成可复用的组件,进一步提高代码复用性和模块化程度。

    11.2.2 属性 (Property) 与属性绑定 (Property Binding)

    属性 (Properties) 是 QML 元素的核心组成部分,用于描述元素的状态和特征,例如位置、大小、颜色、文本内容等。属性绑定 (Property Binding) 是 QML 中一种强大的机制,可以将一个属性的值动态地关联到另一个属性或表达式。当被绑定的属性发生变化时,绑定目标属性会自动更新,实现数据驱动的 UI 效果。

    属性 (Properties)
    QML 元素可以定义多种类型的属性,包括:
    ▮▮▮▮ⓐ 基本类型属性 (Basic Type Properties):例如 int (整型)、real (浮点型)、bool (布尔型)、string (字符串)、color (颜色)、url (URL) 等。
    ▮▮▮▮ⓑ 对象类型属性 (Object Type Properties):例如 font (字体)、border (边框)、gradient (渐变)、anchors (锚点) 等。对象类型属性本身也是对象,可以包含更多的属性。
    ▮▮▮▮ⓒ 信号属性 (Signal Properties):用于定义信号,例如 MouseAreaonClickedonPressed 等信号属性。
    ▮▮▮▮ⓓ 列表属性 (List Properties):用于存储元素列表,例如 ListViewmodel 属性可以绑定一个列表模型。
    ▮▮▮▮ⓔ 自定义属性 (Custom Properties):开发者可以在 QML 代码中自定义属性,用于存储和管理元素的状态。

    定义属性的语法格式如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 property <propertyType> <propertyName>: <initialValue>

    例如,在一个 Rectangle 元素中定义一个自定义属性 count

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 property int count: 0 // 定义整型属性 count,初始值为 0
    3 width: 100
    4 height: 50
    5 color: "lightgray"
    6
    7 // ...
    8 }

    属性绑定 (Property Binding)
    属性绑定是 QML 的核心特性之一,它允许将一个属性的值设置为一个 表达式 (Expression),而不是一个固定的值。当表达式中使用的属性发生变化时,绑定目标属性会自动重新计算并更新。属性绑定的语法格式如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 <propertyName>: <expression>

    表达式可以使用 JavaScript 语法,可以包含其他属性、运算符、函数调用等。例如,将一个 Rectangle 的宽度绑定到其父元素的宽度的一半:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 200
    3 height: 100
    4 color: "white"
    5
    6 Rectangle {
    7 width: parent.width / 2 // 宽度绑定到父元素宽度的一半
    8 height: 50
    9 color: "lightblue"
    10 anchors.centerIn: parent
    11 }
    12 }

    在这个例子中,内部 Rectanglewidth 属性被绑定到 parent.width / 2。当外部 Rectangle 的宽度发生变化时,内部 Rectangle 的宽度也会自动更新,始终保持为父元素宽度的一半。

    属性绑定的优势在于:
    ▮▮▮▮ⓐ 数据驱动 UI (Data-Driven UI):UI 的状态和外观可以完全由数据驱动,当数据发生变化时,UI 会自动更新,无需手动编写更新代码。
    ▮▮▮▮ⓑ 简化动态 UI 开发 (Simplified Dynamic UI Development):属性绑定使得创建动态 UI 变得非常简单,例如根据条件显示或隐藏元素、根据数据动态调整元素位置和大小等。
    ▮▮▮▮ⓒ 提高代码可维护性 (Improved Code Maintainability):属性绑定减少了手动更新 UI 的代码,使得代码更简洁、更易于理解和维护。

    属性绑定可以应用于各种场景,例如:
    ▮▮▮▮ⓐ 尺寸和位置绑定 (Size and Position Binding):将元素的尺寸和位置绑定到其他元素的尺寸和位置,实现相对布局和自适应布局。
    ▮▮▮▮ⓑ 文本内容绑定 (Text Content Binding):将 Text 元素的 text 属性绑定到数据模型或变量,动态显示数据。
    ▮▮▮▮ⓒ 颜色和样式绑定 (Color and Style Binding):根据条件动态改变元素的颜色、样式等。
    ▮▮▮▮ⓓ 动画属性绑定 (Animated Property Binding):将动画的属性绑定到其他属性,实现复杂的动画效果。

    例如,创建一个按钮,其宽度根据文本内容动态调整:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 color: "lightgray"
    3 border.color: "gray"
    4 border.width: 1
    5 height: 40
    6
    7 Text {
    8 id: buttonText
    9 text: "Dynamic Button"
    10 font.pixelSize: 16
    11 anchors.verticalCenter: parent.verticalCenter
    12 anchors.leftMargin: 10
    13 anchors.left: parent.left
    14 }
    15
    16 width: buttonText.width + 20 // 按钮宽度绑定到文本宽度 + 20
    17 MouseArea {
    18 anchors.fill: parent
    19 onClicked: {
    20 console.log("Button Clicked!")
    21 }
    22 }
    23 }

    在这个例子中,Rectanglewidth 属性被绑定到 buttonText.width + 20,按钮的宽度会根据文本内容的宽度自动调整,保持一定的边距。

    总而言之,属性和属性绑定是 QML 语言的核心概念,它们共同构建了 QML 的声明式特性和数据驱动能力。属性用于描述元素的状态,属性绑定则实现了属性值之间的动态关联,使得开发者可以更高效地创建动态、响应式的用户界面。

    11.2.3 信号 (Signal) 与槽 (Slot):QML 中的事件处理

    信号 (Signals)槽 (Slots) 是 Qt 框架的核心机制,用于实现对象之间的 通信 (Communication)事件处理 (Event Handling)。QML 继承了 Qt 的信号与槽机制,并在 QML 元素中广泛使用。信号表示事件的发生,槽是响应信号的函数。当一个信号被发射 (emitted) 时,所有连接到该信号的槽都会被自动调用。

    信号 (Signals)
    信号是对象发出的 通知 (Notifications),表示某种事件已经发生。例如,按钮被点击、鼠标进入区域、属性值改变等都可以作为信号。QML 元素可以定义自己的信号,也可以使用内置元素的信号。定义信号的语法格式如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 signal <signalName>(<parameter1Type> <parameter1Name>, <parameter2Type> <parameter2Name>, ...)

    例如,在一个自定义元素中定义一个名为 valueChanged 的信号,并携带一个整型参数:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Item {
    2 signal valueChanged(int newValue) // 定义信号 valueChanged,携带一个整型参数
    3
    4 function setValue(newValue) {
    5 // ... 一些逻辑 ...
    6 valueChanged(newValue) // 发射信号,传递新的值
    7 }
    8 }

    Qt Quick 内置元素也提供了丰富的信号,例如:
    ▮▮▮▮ⓐ MouseArea 的信号onClickedonPressedonReleasedonEnteredonExitedonPositionChanged 等,用于处理鼠标和触摸事件。
    ▮▮▮▮ⓑ Button 的信号onClicked,用于处理按钮点击事件。
    ▮▮▮▮ⓒ TextInput 的信号onTextChanged,用于处理文本内容改变事件。
    ▮▮▮▮ⓓ Slider 的信号onValueChanged,用于处理滑块值改变事件。
    ▮▮▮▮ⓔ PropertyAnimation 的信号onStartedonStoppedonRunningChanged 等,用于处理动画事件。

    槽 (Slots)
    槽是用于 响应信号 (Response to Signals) 的函数。当一个信号被发射时,所有连接到该信号的槽都会被自动调用。在 QML 中,槽通常使用 JavaScript 函数来实现。连接信号和槽的语法格式如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 <signalSource>.<signalName>.connect(<slotFunction>)

    或者在信号处理程序 (Signal Handler) 中直接编写 JavaScript 代码:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 <signalSource> {
    2 on<SignalName>: {
    3 // JavaScript 代码,作为槽函数
    4 }
    5 }

    例如,连接 MouseAreaonClicked 信号到一个 JavaScript 函数,点击时输出日志:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 100
    3 height: 50
    4 color: "lightgreen"
    5
    6 MouseArea {
    7 anchors.fill: parent
    8 onClicked: { // 信号处理程序,定义槽函数
    9 console.log("Rectangle Clicked!")
    10 }
    11 }
    12 }

    在这个例子中,onClicked: { ... } 就是 MouseAreaclicked 信号的信号处理程序,其中的 JavaScript 代码作为槽函数,响应 clicked 信号。

    信号与槽机制的优势在于:
    ▮▮▮▮ⓐ 松耦合 (Loose Coupling):信号发射者不需要知道哪个对象或槽函数会响应信号,信号和槽之间是松耦合的。
    ▮▮▮▮ⓑ 类型安全 (Type Safety):信号和槽的参数类型在编译时进行检查,保证类型安全。
    ▮▮▮▮ⓒ 灵活性 (Flexibility):一个信号可以连接到多个槽,一个槽也可以连接到多个信号,实现灵活的事件处理和对象通信。

    信号与槽在 QML 中被广泛应用于处理用户交互、组件通信、状态管理、动画控制等各种场景。例如:
    ▮▮▮▮ⓐ 用户交互处理 (User Interaction Handling):使用 MouseArea 的信号处理鼠标点击、移动、按下等事件,响应用户的操作。
    ▮▮▮▮ⓑ 组件通信 (Component Communication):自定义元素可以定义信号,在内部状态变化时发射信号,通知外部组件。外部组件可以连接到这些信号,响应组件的状态变化。
    ▮▮▮▮ⓒ 状态管理 (State Management):使用信号和槽机制实现状态切换和状态通知,例如在状态机 (State Machine) 中,状态切换时发射信号,通知其他组件更新 UI。
    ▮▮▮▮ⓓ 动画控制 (Animation Control):动画元素(如 PropertyAnimation)发射 onStartedonStopped 等信号,可以用于控制动画的启动、停止和状态监听。

    例如,创建一个按钮,点击按钮时发射一个自定义信号:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Item {
    2 signal buttonClicked() // 自定义信号 buttonClicked
    3
    4 Rectangle {
    5 width: 120
    6 height: 40
    7 color: "lightgray"
    8 border.color: "gray"
    9 border.width: 1
    10
    11 Text {
    12 anchors.centerIn: parent
    13 text: "Click Me"
    14 font.pixelSize: 16
    15 }
    16
    17 MouseArea {
    18 anchors.fill: parent
    19 onClicked: {
    20 buttonClicked() // 发射自定义信号
    21 }
    22 }
    23 }
    24 }

    在其他 QML 代码中,可以连接这个自定义信号,响应按钮点击事件:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 MyButtonComponent { // 假设 MyButtonComponent 是上面定义的自定义组件
    2 onButtonClicked: { // 连接自定义信号 buttonClicked
    3 console.log("Custom Button Clicked!")
    4 }
    5 }

    总而言之,信号和槽是 QML 中实现事件处理和对象通信的关键机制。它们提供了松耦合、类型安全、灵活的事件处理方式,使得 QML 能够构建复杂的交互式用户界面。理解和掌握信号与槽机制对于 QML 开发至关重要。

    11.2.4 JavaScript 集成 (JavaScript Integration):在 QML 中编写 JavaScript 代码

    JavaScript 集成 (JavaScript Integration) 是 QML 的一个重要特性,它允许开发者在 QML 代码中嵌入 JavaScript 代码,用于处理逻辑、数据操作和动态行为。JavaScript 在 QML 中扮演着重要的角色,使得 QML 能够处理复杂的交互和动态 UI 效果。

    JavaScript 代码块
    在 QML 中,可以直接在信号处理程序、函数定义、属性绑定等地方编写 JavaScript 代码块。JavaScript 代码块使用花括号 {} 包围。例如,在 MouseAreaonClicked 信号处理程序中编写 JavaScript 代码:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 MouseArea {
    2 anchors.fill: parent
    3 onClicked: { // JavaScript 代码块作为信号处理程序
    4 console.log("Mouse Clicked at x = " + mouseX + ", y = " + mouseY);
    5 // ... 其他 JavaScript 代码 ...
    6 }
    7 }

    JavaScript 函数
    可以在 QML 元素中定义 JavaScript 函数,并在 QML 代码中调用。定义 JavaScript 函数的语法格式如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 function <functionName>(<parameter1Name>, <parameter2Name>, ...) {
    2 // JavaScript 函数体
    3 return <returnValue>; // 可选返回值
    4 }

    例如,定义一个 JavaScript 函数 calculateArea,计算矩形的面积:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 100
    3 height: 50
    4 color: "lightgray"
    5
    6 function calculateArea() { // 定义 JavaScript 函数
    7 return width * height;
    8 }
    9
    10 MouseArea {
    11 anchors.fill: parent
    12 onClicked: {
    13 var area = calculateArea(); // 调用 JavaScript 函数
    14 console.log("Rectangle Area: " + area);
    15 }
    16 }
    17 }

    JavaScript 对象和数据类型
    QML 中的 JavaScript 环境支持标准的 JavaScript 对象和数据类型,例如:
    ▮▮▮▮ⓐ 基本数据类型number (数值)、string (字符串)、boolean (布尔值)、nullundefined
    ▮▮▮▮ⓑ 对象 (Objects):可以使用对象字面量 {} 创建对象,访问对象属性使用点 . 或方括号 []
    ▮▮▮▮ⓒ 数组 (Arrays):可以使用数组字面量 [] 创建数组,访问数组元素使用方括号 [] 和索引。
    ▮▮▮▮ⓓ 函数 (Functions):JavaScript 函数作为一等公民,可以赋值给变量、作为参数传递、作为返回值返回。
    ▮▮▮▮ⓔ 内置对象 (Built-in Objects):例如 Math (数学对象)、Date (日期对象)、JSON (JSON 对象)、console (控制台对象) 等。

    例如,使用 JavaScript 对象和数组处理数据:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 200
    3 height: 100
    4 color: "white"
    5
    6 property var dataList: [ // 定义数组属性
    7 { name: "Apple", price: 2.5 },
    8 { name: "Banana", price: 1.8 },
    9 { name: "Orange", price: 2.0 }
    10 ]
    11
    12 Text {
    13 text: "Fruits:\n" + formatFruitList()
    14 font.pixelSize: 16
    15 }
    16
    17 function formatFruitList() { // JavaScript 函数格式化水果列表
    18 var result = "";
    19 for (var i = 0; i < dataList.length; i++) {
    20 var fruit = dataList[i];
    21 result += fruit.name + " - $" + fruit.price + "\n";
    22 }
    23 return result;
    24 }
    25 }

    访问 QML 属性和元素
    在 JavaScript 代码中,可以直接访问 QML 元素的属性和元素 ID。例如,访问当前元素的属性 widthheight,访问 ID 为 myTextText 元素的 text 属性:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 id: myRect
    3 width: 150
    4 height: 80
    5 color: "lightblue"
    6
    7 Text {
    8 id: myText
    9 text: "Initial Text"
    10 font.pixelSize: 16
    11 anchors.centerIn: parent
    12 }
    13
    14 MouseArea {
    15 anchors.fill: parent
    16 onClicked: {
    17 console.log("Rectangle width: " + myRect.width + ", height: " + myRect.height); // 访问 myRect 的属性
    18 myText.text = "Clicked!"; // 修改 myText 的属性
    19 }
    20 }
    21 }

    调用 C++ 后端功能
    在 Qt Quick 应用中,JavaScript 可以调用 C++ 后端暴露的方法和属性,实现前端与后端的交互。具体的交互方式取决于 C++ 后端如何将功能暴露给 QML,通常使用上下文属性、上下文对象或可调用的 C++ 方法。

    例如,假设 C++ 后端暴露了一个名为 backendObject 的上下文对象,其中包含一个名为 calculateSum 的方法,JavaScript 可以这样调用:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 MouseArea {
    2 anchors.fill: parent
    3 onClicked: {
    4 var result = backendObject.calculateSum(10, 20); // 调用 C++ 后端方法
    5 console.log("Sum from C++ backend: " + result);
    6 }
    7 }

    JavaScript 集成为 QML 提供了强大的逻辑处理能力,使得 QML 能够处理复杂的交互、数据操作和动态 UI 效果。开发者可以充分利用 JavaScript 的灵活性和丰富的库,在 QML 中实现各种应用逻辑和动态行为。然而,对于性能敏感型和复杂的业务逻辑,通常建议放在 C++ 后端处理,以充分发挥 C++ 的性能优势。QML 前端主要负责 UI 呈现和交互逻辑,C++ 后端负责核心业务逻辑和数据处理,两者协同工作,共同构建高性能、功能完善的 Qt Quick 应用。

    11.3 Qt Quick 布局与定位

    11.3.1 定位器 (Positioners):Row, Column, Grid, Flow

    定位器 (Positioners) 是 Qt Quick 提供的一组元素,用于自动布局其子元素。定位器可以根据一定的规则排列子元素,例如水平排列、垂直排列、网格排列、流式排列等,简化了布局管理,使得 UI 能够自适应不同的屏幕尺寸和分辨率。Qt Quick 提供了以下几种常用的定位器:

    Row 定位器
    Row 定位器将其子元素 水平排列 (Horizontally) 成一行。Row 定位器的主要属性包括:
    ▮▮▮▮ⓐ spacing:子元素之间的水平间距。
    ▮▮▮▮ⓑ layoutDirection:布局方向,例如 Qt.LeftToRight (从左到右,默认)、Qt.RightToLeft (从右到左)。
    ▮▮▮▮ⓒ clip:是否裁剪超出定位器边界的子元素,默认为 false

    例如,使用 Row 定位器水平排列三个矩形:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Row {
    2 spacing: 10 // 子元素水平间距 10 像素
    3
    4 Rectangle { width: 50; height: 50; color: "red" }
    5 Rectangle { width: 50; height: 50; color: "green" }
    6 Rectangle { width: 50; height: 50; color: "blue" }
    7 }

    这段代码将三个矩形水平排列成一行,每个矩形之间有 10 像素的水平间距。

    Column 定位器
    Column 定位器将其子元素 垂直排列 (Vertically) 成一列。Column 定位器的主要属性包括:
    ▮▮▮▮ⓐ spacing:子元素之间的垂直间距。
    ▮▮▮▮ⓑ layoutDirection:布局方向,例如 Qt.TopToBottom (从上到下,默认)、Qt.BottomToTop (从下到上)。
    ▮▮▮▮ⓒ clip:是否裁剪超出定位器边界的子元素,默认为 false

    例如,使用 Column 定位器垂直排列三个矩形:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Column {
    2 spacing: 10 // 子元素垂直间距 10 像素
    3
    4 Rectangle { width: 50; height: 50; color: "red" }
    5 Rectangle { width: 50; height: 50; color: "green" }
    6 Rectangle { width: 50; height: 50; color: "blue" }
    7 }

    这段代码将三个矩形垂直排列成一列,每个矩形之间有 10 像素的垂直间距。

    Grid 定位器
    Grid 定位器将其子元素排列成 网格 (Grid) 状。Grid 定位器的主要属性包括:
    ▮▮▮▮ⓐ columns:网格的列数。
    ▮▮▮▮ⓑ rows:网格的行数(可选,通常根据子元素数量自动计算)。
    ▮▮▮▮ⓒ columnSpacing:列之间的水平间距。
    ▮▮▮▮ⓓ rowSpacing:行之间的垂直间距。
    ▮▮▮▮ⓔ flow:子元素的排列顺序,例如 Grid.LeftToRight (从左到右,逐行填充,默认)、Grid.TopToBottom (从上到下,逐列填充)。
    ▮▮▮▮ⓕ clip:是否裁剪超出定位器边界的子元素,默认为 false

    例如,使用 Grid 定位器将 9 个矩形排列成 3x3 的网格:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Grid {
    2 columns: 3 // 3 列网格
    3 columnSpacing: 5 // 列间距 5 像素
    4 rowSpacing: 5 // 行间距 5 像素
    5
    6 Rectangle { width: 40; height: 40; color: "red" }
    7 Rectangle { width: 40; height: 40; color: "green" }
    8 Rectangle { width: 40; height: 40; color: "blue" }
    9 Rectangle { width: 40; height: 40; color: "yellow" }
    10 Rectangle { width: 40; height: 40; color: "cyan" }
    11 Rectangle { width: 40; height: 40; color: "magenta" }
    12 Rectangle { width: 40; height: 40; color: "gray" }
    13 Rectangle { width: 40; height: 40; color: "lightgray" }
    14 Rectangle { width: 40; height: 40; color: "darkgray" }
    15 }

    这段代码将 9 个矩形排列成 3 列 3 行的网格,列间距和行间距均为 5 像素。

    Flow 定位器
    Flow 定位器将其子元素 流式排列 (Flow Layout),类似于文本的自动换行效果。当一行或一列空间不足时,子元素会自动换行到下一行或下一列。Flow 定位器的主要属性包括:
    ▮▮▮▮ⓐ flow:流式布局的方向,例如 Flow.LeftToRight (从左到右,水平流式,默认)、Flow.TopToBottom (从上到下,垂直流式)。
    ▮▮▮▮ⓑ spacing:子元素之间的水平和垂直间距。
    ▮▮▮▮ⓒ itemSpacing:子元素之间的间距,可以同时设置水平和垂直间距。
    ▮▮▮▮ⓓ clip:是否裁剪超出定位器边界的子元素,默认为 false

    例如,使用 Flow 定位器流式排列多个不同宽度的矩形:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Flow {
    2 width: 200 // Flow 定位器宽度固定为 200 像素
    3 spacing: 5 // 子元素间距 5 像素
    4
    5 Rectangle { width: 80; height: 40; color: "red" }
    6 Rectangle { width: 60; height: 40; color: "green" }
    7 Rectangle { width: 100; height: 40; color: "blue" }
    8 Rectangle { width: 70; height: 40; color: "yellow" }
    9 Rectangle { width: 90; height: 40; color: "cyan" }
    10 Rectangle { width: 50; height: 40; color: "magenta" }
    11 }

    这段代码将多个不同宽度的矩形在宽度为 200 像素的 Flow 定位器中流式排列。当一行空间不足时,矩形会自动换行到下一行。

    定位器的优势在于:
    ▮▮▮▮ⓐ 简化布局代码 (Simplified Layout Code):使用定位器可以大大简化布局代码,无需手动计算和设置子元素的位置。
    ▮▮▮▮ⓑ 自适应布局 (Adaptive Layout):定位器可以根据子元素的内容和定位器的尺寸自动调整布局,实现自适应布局效果。
    ▮▮▮▮ⓒ 易于维护 (Easy to Maintain):使用定位器布局的代码更易于阅读和维护,修改布局规则时只需调整定位器的属性。

    定位器通常用于构建简单的线性布局或网格布局。对于更复杂的布局需求,可以结合使用 锚点 (Anchors)布局项 (Layout Items),实现更精细的布局控制。定位器可以嵌套使用,例如在一个 Column 定位器中嵌套多个 Row 定位器,构建更复杂的复合布局。

    11.3.2 锚点 (Anchors):相对定位

    锚点 (Anchors) 是 Qt Quick 中用于 相对定位 (Relative Positioning) 元素的机制。通过锚点,可以将一个元素的边缘或中心点与另一个元素的边缘或中心点对齐,实现元素之间的相对位置关系。锚点提供了灵活的定位方式,使得 UI 能够自适应不同的屏幕尺寸和布局变化。

    锚点属性
    每个 QML 元素都有一组 anchors 属性anchors 属性本身也是一个对象,包含以下常用的锚点:
    ▮▮▮▮ⓐ anchors.top:元素的上边缘。
    ▮▮▮▮ⓑ anchors.bottom:元素的下边缘。
    ▮▮▮▮ⓒ anchors.left:元素的左边缘。
    ▮▮▮▮ⓓ anchors.right:元素的右边缘。
    ▮▮▮▮ⓔ anchors.verticalCenter:元素的垂直中心线。
    ▮▮▮▮ⓕ anchors.horizontalCenter:元素的水平中心线。
    ▮▮▮▮ⓖ anchors.baseline:元素的文本基线(对于 Text 元素等)。
    ▮▮▮▮ⓗ anchors.fill:元素的填充区域,通常用于使其填充父元素。
    ▮▮▮▮ⓘ anchors.centerIn:元素的中心点,通常用于使其居中于父元素。

    锚点值
    可以将一个元素的锚点属性绑定到另一个元素的锚点属性,实现相对定位。锚点值通常是 锚点目标 (Anchor Target)偏移量 (Offset) 的组合。锚点目标可以是:
    ▮▮▮▮ⓐ parent:父元素。
    ▮▮▮▮ⓑ 其他元素的 id:使用元素的 id 引用其他元素。

    偏移量可以使用 anchors.topMarginanchors.bottomMarginanchors.leftMarginanchors.rightMargin 等属性设置。

    常用锚点定位方式
    ▮▮▮▮ⓑ 对齐边缘 (Align Edges):将一个元素的边缘与另一个元素的边缘对齐。例如,将一个矩形的左边缘与父元素的左边缘对齐,上边缘与父元素的上边缘对齐:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 200
    3 height: 100
    4 color: "white"
    5
    6 Rectangle {
    7 width: 50
    8 height: 50
    9 color: "lightblue"
    10 anchors.left: parent.left // 左边缘与父元素左边缘对齐
    11 anchors.top: parent.top // 上边缘与父元素上边缘对齐
    12 anchors.leftMargin: 10 // 左边距 10 像素
    13 anchors.topMargin: 10 // 上边距 10 像素
    14 }
    15 }

    这段代码将内部矩形定位在父元素的左上角,并留出 10 像素的边距。

    ▮▮▮▮ⓑ 居中对齐 (Center Alignment):将一个元素的中心点与另一个元素的中心点对齐。例如,将一个文本标签居中于父元素:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 200
    3 height: 100
    4 color: "white"
    5
    6 Text {
    7 text: "Centered Text"
    8 font.pixelSize: 20
    9 anchors.centerIn: parent // 中心点与父元素中心点对齐
    10 }
    11 }

    这段代码将文本标签水平和垂直居中于父元素。

    ▮▮▮▮ⓒ 填充父元素 (Fill Parent):使一个元素完全填充其父元素的区域。例如,将一个 MouseArea 填充其父元素,使其成为整个父元素的可点击区域:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 100
    3 height: 50
    4 color: "lightgreen"
    5
    6 MouseArea {
    7 anchors.fill: parent // 填充父元素
    8 onClicked: {
    9 console.log("Rectangle Clicked!")
    10 }
    11 }
    12 }

    这段代码使得 MouseArea 完全覆盖了 Rectangle 的区域,整个矩形都可点击。

    ▮▮▮▮ⓓ 相对其他元素定位 (Relative to Other Elements):将一个元素相对于另一个元素定位。例如,将一个按钮定位在另一个文本标签的右侧:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 300
    3 height: 50
    4 color: "white"
    5
    6 Text {
    7 id: label
    8 text: "Label:"
    9 font.pixelSize: 16
    10 anchors.left: parent.left // 左边缘与父元素左边缘对齐
    11 anchors.verticalCenter: parent.verticalCenter // 垂直中心线与父元素垂直中心线对齐
    12 anchors.leftMargin: 10
    13 }
    14
    15 Button {
    16 text: "Button"
    17 anchors.left: label.right // 左边缘与 label 的右边缘对齐
    18 anchors.verticalCenter: parent.verticalCenter // 垂直中心线与父元素垂直中心线对齐
    19 anchors.leftMargin: 5 // 左边距 5 像素
    20 }
    21 }

    这段代码将按钮定位在文本标签 label 的右侧,并保持垂直居中。

    锚点的优势在于:
    ▮▮▮▮ⓐ 灵活的相对定位 (Flexible Relative Positioning):锚点提供了多种相对定位方式,可以实现各种复杂的布局需求。
    ▮▮▮▮ⓑ 自适应布局 (Adaptive Layout):使用锚点布局的 UI 可以根据父元素的尺寸变化自动调整子元素的位置,实现自适应布局效果。
    ▮▮▮▮ⓒ 易于维护 (Easy to Maintain):使用锚点布局的代码更易于阅读和维护,修改布局规则时只需调整锚点属性。

    锚点通常用于构建复杂的相对布局,例如将元素放置在窗口的特定位置、相对于其他元素定位、创建自适应布局等。锚点可以与定位器结合使用,例如在定位器中使用锚点调整子元素的位置,实现更精细的布局控制。

    11.3.3 布局项 (Layout Items):Layout 属性

    布局项 (Layout Items) 是 Qt Quick 中用于更精细地控制元素在布局中的行为和尺寸的机制。布局项通过 Layout 属性 提供了一组附加属性,可以应用于定位器 (如 RowColumnGridFlow) 的子元素,影响子元素在布局中的尺寸、对齐方式、伸缩比例等。布局项提供了比定位器更细粒度的布局控制。

    Layout 附加属性
    Layout 是一个 附加对象 (Attached Object),可以附加到定位器的子元素上。Layout 附加属性主要包括:
    ▮▮▮▮ⓐ Layout.preferredWidthLayout.preferredHeight:元素的 首选宽度 (Preferred Width)首选高度 (Preferred Height)。定位器在布局时会尽量尊重元素的首选尺寸。
    ▮▮▮▮ⓑ Layout.minimumWidthLayout.minimumHeight:元素的 最小宽度 (Minimum Width)最小高度 (Minimum Height)。元素在布局中不能小于最小尺寸。
    ▮▮▮▮ⓒ Layout.maximumWidthLayout.maximumHeight:元素的 最大宽度 (Maximum Width)最大高度 (Maximum Height)。元素在布局中不能超过最大尺寸。
    ▮▮▮▮ⓓ Layout.fillWidthLayout.fillHeight:元素的 宽度伸缩比例 (Width Fill Factor)高度伸缩比例 (Height Fill Factor)。用于控制元素在定位器剩余空间中的伸缩比例。
    ▮▮▮▮ⓔ Layout.alignment:元素在布局单元格内的 对齐方式 (Alignment)。例如 Qt.AlignLeftQt.AlignRightQt.AlignTopQt.AlignBottomQt.AlignCenter 等。
    ▮▮▮▮ⓕ Layout.rowLayout.column (仅用于 Grid 定位器):元素在 Grid 网格中的 行索引 (Row Index)列索引 (Column Index)
    ▮▮▮▮ⓖ Layout.rowSpanLayout.columnSpan (仅用于 Grid 定位器):元素在 Grid 网格中占用的 行跨度 (Row Span)列跨度 (Column Span)

    使用 Layout 属性控制尺寸
    ▮▮▮▮ⓑ 首选尺寸 (Preferred Size):使用 Layout.preferredWidthLayout.preferredHeight 设置元素的首选尺寸。定位器在布局时会尽量尊重元素的首选尺寸,但可能会根据定位器的可用空间进行调整。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Row {
    2 spacing: 10
    3
    4 Rectangle {
    5 Layout.preferredWidth: 80 // 首选宽度 80 像素
    6 Layout.preferredHeight: 40 // 首选高度 40 像素
    7 color: "red"
    8 }
    9
    10 Rectangle {
    11 Layout.preferredWidth: 120 // 首选宽度 120 像素
    12 Layout.preferredHeight: 40 // 首选高度 40 像素
    13 color: "green"
    14 }
    15 }

    ▮▮▮▮ⓑ 最小/最大尺寸 (Minimum/Maximum Size):使用 Layout.minimumWidthLayout.minimumHeightLayout.maximumWidthLayout.maximumHeight 设置元素的最小和最大尺寸。元素在布局中不会超出尺寸范围。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 width: 200
    3 height: 50
    4 color: "white"
    5
    6 Row {
    7 anchors.fill: parent
    8 spacing: 10
    9
    10 Rectangle {
    11 Layout.minimumWidth: 50 // 最小宽度 50 像素
    12 Layout.maximumWidth: 100 // 最大宽度 100 像素
    13 Layout.fillWidth: 1 // 宽度伸缩比例 1
    14 height: 40
    15 color: "red"
    16 }
    17
    18 Rectangle {
    19 Layout.minimumWidth: 60 // 最小宽度 60 像素
    20 Layout.maximumWidth: 120 // 最大宽度 120 像素
    21 Layout.fillWidth: 2 // 宽度伸缩比例 2
    22 height: 40
    23 color: "green"
    24 }
    25 }
    26 }

    在这个例子中,两个矩形在 Row 定位器中水平排列,它们的宽度会根据 Layout.fillWidth 属性的比例进行伸缩,但不会超出最小和最大宽度限制。

    使用 Layout 属性控制伸缩比例
    Layout.fillWidthLayout.fillHeight 属性用于控制元素在定位器剩余空间中的伸缩比例。伸缩比例越大,元素占据的剩余空间越多。伸缩比例为 0 表示不伸缩。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Row {
    2 width: 300
    3 spacing: 10
    4
    5 Rectangle {
    6 Layout.fillWidth: 1 // 宽度伸缩比例 1
    7 height: 40
    8 color: "red"
    9 }
    10
    11 Rectangle {
    12 Layout.fillWidth: 2 // 宽度伸缩比例 2
    13 height: 40
    14 color: "green"
    15 }
    16
    17 Rectangle {
    18 Layout.fillWidth: 1 // 宽度伸缩比例 1
    19 height: 40
    20 color: "blue"
    21 }
    22 }

    在这个例子中,三个矩形在 Row 定位器中水平排列,它们的宽度伸缩比例分别为 1:2:1,它们会根据这个比例瓜分 Row 定位器的剩余宽度。

    使用 Layout 属性控制对齐方式
    Layout.alignment 属性用于设置元素在布局单元格内的对齐方式。对齐方式可以是水平对齐 (如 Qt.AlignLeftQt.AlignRightQt.AlignHCenter) 或垂直对齐 (如 Qt.AlignTopQt.AlignBottomQt.AlignVCenter),也可以是组合对齐 (如 Qt.AlignTop | Qt.AlignLeft)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Grid {
    2 columns: 2
    3 columnSpacing: 10
    4 rowSpacing: 10
    5
    6 Rectangle { width: 80; height: 40; color: "red" } // 默认对齐方式
    7
    8 Rectangle {
    9 Layout.alignment: Qt.AlignRight | Qt.AlignBottom // 右下角对齐
    10 width: 80; height: 40; color: "green"
    11 }
    12
    13 Rectangle {
    14 Layout.alignment: Qt.AlignHCenter // 水平居中对齐
    15 width: 80; height: 40; color: "blue"
    16 }
    17
    18 Rectangle {
    19 Layout.alignment: Qt.AlignVCenter // 垂直居中对齐
    20 width: 80; height: 40; color: "yellow"
    21 }
    22 }

    布局项的优势在于:
    ▮▮▮▮ⓐ 精细的布局控制 (Fine-grained Layout Control):布局项提供了比定位器更细粒度的布局控制,可以更精确地控制元素在布局中的尺寸、伸缩、对齐等行为。
    ▮▮▮▮ⓑ 灵活的布局策略 (Flexible Layout Strategies):布局项可以与定位器结合使用,实现各种复杂的布局策略,例如比例伸缩、最小/最大尺寸限制、对齐方式控制等。
    ▮▮▮▮ⓒ 提高布局灵活性 (Improved Layout Flexibility):使用布局项可以提高布局的灵活性和可维护性,使得 UI 能够更好地适应不同的屏幕尺寸和布局需求.

    布局项通常与定位器一起使用,用于构建更复杂、更精细的布局。定位器负责元素的整体排列,布局项负责元素的个体行为控制。合理使用布局项可以使 QML 布局更加强大和灵活。

    11.4 Qt Quick 动画与特效

    11.4.1 属性动画 (Property Animation):NumberAnimation, ColorAnimation, RotationAnimation

    属性动画 (Property Animation) 是 Qt Quick 中最常用的动画类型之一。属性动画可以平滑地改变 QML 元素的属性值,例如位置、大小、颜色、透明度、旋转角度等,从而创建流畅、自然的动画效果。Qt Quick 提供了多种预定义的属性动画类型,例如 NumberAnimationColorAnimationRotationAnimation 等,用于不同类型的属性动画。

    NumberAnimation 类型
    NumberAnimation 用于动画化 数值类型 (Number Type) 的属性,例如 xywidthheightopacity 等。NumberAnimation 的主要属性包括:
    ▮▮▮▮ⓐ target:动画的目标元素。
    ▮▮▮▮ⓑ property:要动画化的属性名称(字符串)。
    ▮▮▮▮ⓒ from:动画的起始值(可选,默认为当前属性值)。
    ▮▮▮▮ⓓ to:动画的结束值。
    ▮▮▮▮ⓔ duration:动画的持续时间,单位为毫秒。
    ▮▮▮▮ⓕ easing.type:动画的缓动函数类型,例如 Easing.Linear (线性)、Easing.InOutQuad (缓入缓出二次方) 等。缓动函数控制动画的速度变化曲线,影响动画的视觉效果。
    ▮▮▮▮ⓖ loops:动画的循环次数,-1 表示无限循环。
    ▮▮▮▮ⓗ running:动画是否正在运行,可以控制动画的启动和停止。
    ▮▮▮▮ⓘ onStartedonStoppedonRunningChanged 等信号:动画的事件信号,例如动画开始、停止、运行状态改变时发射信号。

    例如,创建一个矩形,点击时平滑移动到新的位置:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 id: rect
    3 width: 100
    4 height: 100
    5 color: "red"
    6 x: 50
    7 y: 50
    8
    9 MouseArea {
    10 anchors.fill: parent
    11 onClicked: {
    12 rect.x = Math.random() * (parent.width - rect.width)
    13 rect.y = Math.random() * (parent.height - rect.height)
    14 moveAnimation.start() // 启动动画
    15 }
    16 }
    17
    18 NumberAnimation {
    19 id: moveAnimation
    20 target: rect
    21 properties: "x,y" // 同时动画化 x 和 y 属性
    22 to: { x: rect.x, y: rect.y } // 结束值动态绑定到矩形的当前 x 和 y 属性
    23 duration: 500
    24 easing.type: Easing.InOutQuad
    25 }
    26 }

    在这个例子中,moveAnimation 定义了一个平滑移动矩形的动画。当鼠标点击矩形时,矩形会动画地移动到一个随机的新位置。properties: "x,y" 表示同时动画化 xy 属性。

    ColorAnimation 类型
    ColorAnimation 用于动画化 颜色类型 (Color Type) 的属性,例如 colorbackgroundColorborderColor 等。ColorAnimation 的属性与 NumberAnimation 类似,只是 fromto 属性的值是颜色值。

    例如,创建一个矩形,鼠标悬停时颜色平滑过渡到蓝色:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 id: colorRect
    3 width: 100
    4 height: 100
    5 color: "red"
    6
    7 MouseArea {
    8 anchors.fill: parent
    9 hoverEnabled: true // 启用悬停事件
    10 onEntered: {
    11 colorAnimation.to = "blue" // 鼠标进入时设置动画结束颜色为蓝色
    12 colorAnimation.start() // 启动颜色动画
    13 }
    14 onExited: {
    15 colorAnimation.to = "red" // 鼠标离开时设置动画结束颜色为红色
    16 colorAnimation.start() // 启动颜色动画
    17 }
    18 }
    19
    20 ColorAnimation {
    21 id: colorAnimation
    22 target: colorRect
    23 property: "color"
    24 duration: 300
    25 easing.type: Easing.Linear
    26 }
    27 }

    这段代码中,colorAnimation 定义了一个颜色过渡动画。当鼠标进入矩形区域时,矩形的颜色会平滑地从红色过渡到蓝色;当鼠标离开时,颜色会平滑地过渡回红色。

    RotationAnimation 类型
    RotationAnimation 用于动画化 旋转角度 (Rotation Angle) 属性,通常用于 rotation 属性。RotationAnimation 的属性与 NumberAnimation 类似,只是动画化的属性是旋转角度。

    例如,创建一个矩形,点击时旋转 360 度:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 id: rotateRect
    3 width: 100
    4 height: 100
    5 color: "lightgray"
    6 rotation: 0 // 初始旋转角度为 0 度
    7
    8 MouseArea {
    9 anchors.fill: parent
    10 onClicked: {
    11 rotationAnimation.to = rotateRect.rotation + 360 // 每次点击增加 360 度旋转
    12 rotationAnimation.start() // 启动旋转动画
    13 }
    14 }
    15
    16 RotationAnimation {
    17 id: rotationAnimation
    18 target: rotateRect
    19 property: "rotation"
    20 duration: 1000
    21 easing.type: Easing.InOutQuad
    22 }
    23 }

    这段代码中,rotationAnimation 定义了一个旋转动画。每次点击矩形时,矩形会平滑地旋转 360 度。

    除了以上介绍的 NumberAnimationColorAnimationRotationAnimation,Qt Quick 还提供了其他属性动画类型,例如 ScaleAnimation (缩放动画)、TranslateAnimation (平移动画)、OpacityAnimation (透明度动画) 等,用于动画化不同类型的属性。

    属性动画的优势在于:
    ▮▮▮▮ⓐ 简洁的动画定义 (Concise Animation Definition):使用属性动画可以简洁地定义动画效果,无需编写复杂的命令式动画代码。
    ▮▮▮▮ⓑ 声明式动画 (Declarative Animation):属性动画是声明式定义的,只需描述动画的目标属性、起始值、结束值、持续时间等,Qt Quick 引擎会自动处理动画的播放和更新。
    ▮▮▮▮ⓒ 易于控制 (Easy to Control):属性动画提供了丰富的属性和信号,可以方便地控制动画的启动、停止、循环、速度、缓动效果等。

    属性动画通常用于创建简单的属性过渡动画,例如元素的位置移动、大小缩放、颜色变化、旋转等。对于更复杂的动画效果,可以组合使用多种属性动画,或者使用 序列动画 (Sequential Animation)并行动画 (Parallel Animation) 等高级动画类型。

    11.4.2 过渡效果 (Transitions):Animated Property Changes

    过渡效果 (Transitions) 是 Qt Quick 中一种方便的动画机制,用于在 属性值发生变化时自动应用动画效果。与属性动画需要手动启动不同,过渡效果是 自动触发 (Automatically Triggered) 的,当指定的属性值发生变化时,过渡效果会自动播放动画,实现平滑的属性过渡。

    transitions 属性
    QML 元素通常有一个 transitions 属性,用于定义应用于该元素的过渡效果。transitions 属性是一个 过渡效果列表 (List of Transitions),可以包含多个过渡效果,每个过渡效果可以针对不同的属性或条件。定义过渡效果列表的语法格式如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 <elementType> {
    2 transitions: [
    3 Transition {
    4 // 过渡效果定义 1
    5 },
    6 Transition {
    7 // 过渡效果定义 2
    8 },
    9 // ...
    10 ]
    11 // ...
    12 }

    Transition 元素
    Transition 元素用于定义一个具体的过渡效果。Transition 元素的主要属性包括:
    ▮▮▮▮ⓐ target:过渡效果的目标元素(可选,如果省略则默认为定义 transitions 属性的元素)。
    ▮▮▮▮ⓑ properties:触发过渡效果的属性列表(字符串列表)。当列表中的任何一个属性值发生变化时,过渡效果会被触发。
    ▮▮▮▮ⓒ fromto:触发过渡效果的属性值的范围(可选)。可以使用 * 表示任意值。例如,from: "*" 表示从任意值开始过渡,to: true 表示过渡到 true 值。
    ▮▮▮▮ⓓ 动画定义:在 Transition 元素内部,可以定义一个或多个属性动画,例如 NumberAnimationColorAnimationRotationAnimation 等,用于描述属性过渡的动画效果。

    例如,创建一个矩形,当 state 属性从 "" 变为 "active" 时,颜色平滑过渡到蓝色:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 id: transitionRect
    3 width: 100
    4 height: 100
    5 color: "red"
    6 state: "" // 初始状态为空字符串
    7
    8 states: State {
    9 name: "active"
    10 PropertyChanges { target: transitionRect; color: "blue" } // 状态 "active" 时颜色变为蓝色
    11 }
    12
    13 transitions: Transition { // 定义状态过渡效果
    14 target: transitionRect
    15 properties: "color" // 监听 color 属性的变化
    16 from: "" // 从任意状态开始过渡
    17 to: "active" // 过渡到状态 "active"
    18 ColorAnimation { duration: 500; easing.type: Easing.Linear } // 颜色动画效果
    19 }
    20
    21 MouseArea {
    22 anchors.fill: parent
    23 onClicked: {
    24 transitionRect.state = (transitionRect.state === "active" ? "" : "active") // 切换状态
    25 }
    26 }
    27 }

    在这个例子中,transitions 属性定义了一个过渡效果。当 color 属性发生变化,并且从任意状态过渡到 "active" 状态时,会播放一个 ColorAnimation,使颜色从当前值平滑过渡到蓝色。当点击矩形时,state 属性会在 "" 和 "active" 之间切换,触发颜色过渡动画。

    属性值范围条件
    Transition 元素的 fromto 属性可以用于指定触发过渡效果的属性值范围。可以使用具体的值、* (任意值) 或状态名称。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 transitions: [
    2 Transition { // 从红色过渡到蓝色
    3 properties: "color"
    4 from: "red"
    5 to: "blue"
    6 ColorAnimation { duration: 500 }
    7 },
    8 Transition { // 从任意颜色过渡到绿色
    9 properties: "color"
    10 from: "*"
    11 to: "green"
    12 ColorAnimation { duration: 300 }
    13 }
    14 ]

    这段代码定义了两个过渡效果。第一个过渡效果只在颜色从红色变为蓝色时触发,第二个过渡效果在颜色变为绿色时触发,不论之前的颜色是什么。

    过渡效果的优势在于:
    ▮▮▮▮ⓐ 自动触发动画 (Automatic Animation Triggering):过渡效果是自动触发的,无需手动启动动画,当属性值发生变化时,动画会自动播放。
    ▮▮▮▮ⓑ 简化状态动画 (Simplified State Animation):过渡效果可以方便地实现状态切换时的属性动画,使得状态动画的定义更加简洁。
    ▮▮▮▮ⓒ 声明式过渡 (Declarative Transition):过渡效果是声明式定义的,只需描述触发条件和动画效果,Qt Quick 引擎会自动处理过渡的播放和管理。

    过渡效果通常用于实现状态切换时的平滑过渡动画,例如按钮的悬停效果、状态切换时的颜色或位置变化、组件的显示和隐藏动画等。过渡效果可以与 状态 (States)状态组 (State Groups) 结合使用,构建更复杂的状态动画效果。

    11.4.3 状态 (States) 与状态组 (State Groups):复杂动画的组织与管理

    状态 (States)状态组 (State Groups) 是 Qt Quick 中用于 组织和管理复杂动画 的机制。状态允许为一个元素定义不同的状态,每个状态可以修改元素的属性值。状态组则用于组织多个状态,实现更复杂的状态逻辑和动画效果。状态和状态组提供了一种结构化的方式来管理 UI 的不同状态和状态之间的动画过渡。

    状态 (States)
    QML 元素通常有一个 states 属性,用于定义该元素的不同状态。states 属性是一个 状态列表 (List of States),可以包含多个 State 元素。每个 State 元素定义一个状态,并可以包含 属性改变 (Property Changes)信号连接 (Signal Connections)动画定义 (Animation Definitions)。定义状态列表的语法格式如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 <elementType> {
    2 states: [
    3 State {
    4 name: "<stateName1>"
    5 PropertyChanges {
    6 // 属性改变定义
    7 }
    8 // SignalConnections {
    9 // // 信号连接定义
    10 // }
    11 // ...
    12 },
    13 State {
    14 name: "<stateName2>"
    15 PropertyChanges {
    16 // 属性改变定义
    17 }
    18 // ...
    19 },
    20 // ...
    21 ]
    22 // ...
    23 }

    State 元素
    State 元素用于定义一个具体的状态。State 元素的主要属性包括:
    ▮▮▮▮ⓐ name:状态的名称(字符串),用于在 QML 代码中引用状态。
    ▮▮▮▮ⓑ PropertyChanges 元素:定义当元素进入该状态时,需要修改的属性值。PropertyChanges 元素包含一系列属性赋值语句,例如 PropertyChanges { target: <targetElement>; <propertyName1>: <newValue1>; <propertyName2>: <newValue2>; ... }
    ▮▮▮▮ⓒ SignalConnections 元素:定义当元素进入该状态时,需要建立的信号连接。SignalConnections 元素包含一系列信号连接语句,例如 SignalConnections { target: <signalSource>; on<SignalName>: <slotFunction>; ... }
    ▮▮▮▮ⓓ when 属性:可选属性,用于指定状态激活的条件(布尔表达式)。只有当 when 属性为 true 时,状态才会被激活。

    例如,创建一个按钮,具有 "normal" 和 "pressed" 两种状态,点击时切换状态并改变颜色和大小:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 id: stateButton
    3 width: 120
    4 height: 40
    5 color: "lightgray"
    6 border.color: "gray"
    7 border.width: 1
    8 state: "normal" // 初始状态为 "normal"
    9
    10 Text {
    11 anchors.centerIn: parent
    12 text: "Click Me"
    13 font.pixelSize: 16
    14 }
    15
    16 MouseArea {
    17 anchors.fill: parent
    18 onPressed: { stateButton.state = "pressed" } // 鼠标按下时切换到 "pressed" 状态
    19 onReleased: { stateButton.state = "normal" } // 鼠标释放时切换回 "normal" 状态
    20 }
    21
    22 states: [
    23 State {
    24 name: "normal"
    25 PropertyChanges {
    26 target: stateButton
    27 color: "lightgray"
    28 scale: 1.0
    29 }
    30 },
    31 State {
    32 name: "pressed"
    33 PropertyChanges {
    34 target: stateButton
    35 color: "darkgray"
    36 scale: 0.95 // 状态 "pressed" 时缩小 5%
    37 }
    38 }
    39 ]
    40
    41 transitions: Transition { // 定义状态过渡效果
    42 PropertyAnimation { properties: "color,scale"; duration: 100 } // 颜色和缩放动画
    43 }
    44 }

    在这个例子中,states 属性定义了 "normal" 和 "pressed" 两种状态。当状态切换时,PropertyChanges 元素会修改 colorscale 属性的值。transitions 属性定义了状态过渡动画,使得状态切换更加平滑。

    状态组 (State Groups)
    状态组 (State Groups) 用于组织多个状态,实现更复杂的状态逻辑和动画效果。状态组使用 StateGroup 元素定义,可以包含多个 State 元素。状态组的主要优势在于可以实现 互斥状态 (Mutually Exclusive States),即在同一个状态组中,同时只能激活一个状态。

    状态组通常与 属性值条件 (Property Value Conditions)JavaScript 逻辑 (JavaScript Logic) 结合使用,根据条件动态切换状态。例如,使用 Switch 元素和 StateGroup 实现一个开关按钮,具有 "on" 和 "off" 两种互斥状态:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 Rectangle {
    2 id: switchButton
    3 width: 80
    4 height: 40
    5 color: "lightgray"
    6 border.color: "gray"
    7 border.width: 1
    8 property bool checked: false // 自定义属性 checked,表示开关状态
    9
    10 Rectangle { // 滑块
    11 id: slider
    12 width: 30
    13 height: 30
    14 radius: 15
    15 color: "white"
    16 anchors.verticalCenter: parent.verticalCenter
    17 x: 5 // 初始位置
    18
    19 states: StateGroup {
    20 states: [
    21 State { // "off" 状态
    22 name: "off"
    23 when: !switchButton.checked // 当 checked 为 false 时激活
    24 PropertyChanges { target: slider; x: 5 }
    25 },
    26 State { // "on" 状态
    27 name: "on"
    28 when: switchButton.checked // 当 checked 为 true 时激活
    29 PropertyChanges { target: slider; x: switchButton.width - slider.width - 5 }
    30 }
    31 ]
    32 }
    33
    34 transitions: Transition { // 滑块位置过渡动画
    35 NumberAnimation { properties: "x"; duration: 200; easing.type: Easing.InOutQuad }
    36 }
    37 }
    38
    39 MouseArea {
    40 anchors.fill: parent
    41 onClicked: { switchButton.checked = !switchButton.checked } // 点击时切换 checked 属性
    42 }
    43 }

    在这个例子中,StateGroup 定义了 "off" 和 "on" 两种互斥状态,通过 when 属性根据 checked 属性的值动态切换状态。当状态切换时,滑块的位置会平滑过渡,实现开关按钮的动画效果。

    状态和状态组的优势在于:
    ▮▮▮▮ⓐ 结构化状态管理 (Structured State Management):状态和状态组提供了一种结构化的方式来管理 UI 的不同状态,使得复杂的状态逻辑更易于组织和维护。
    ▮▮▮▮ⓑ 互斥状态 (Mutually Exclusive States):状态组可以实现互斥状态,确保在同一时刻只有一个状态被激活,避免状态冲突和逻辑错误。
    ▮▮▮▮ⓒ 复杂的动画组织 (Complex Animation Organization):状态和状态组可以与过渡效果和属性动画结合使用,构建复杂的、多状态的动画效果,例如 UI 组件的不同交互状态、动画场景的切换等。

    状态和状态组通常用于构建具有多种交互状态和复杂动画效果的 UI 组件和应用场景,例如自定义控件、游戏界面、复杂动画场景等。合理使用状态和状态组可以使 QML 代码更加模块化、可维护和易于扩展。

    11.4.4 粒子效果 (Particle Effects):创建炫酷的视觉效果

    粒子效果 (Particle Effects) 是 Qt Quick 中一种用于创建 炫酷视觉效果 (Visually Appealing Effects) 的技术。粒子效果通过模拟大量微小粒子(例如点、线、图像)的运动和行为,可以创建出各种动态的视觉效果,例如火焰、烟雾、星尘、爆炸、水花等。粒子效果可以显著提升 UI 的视觉吸引力和用户体验。

    粒子系统 (Particle System) 的基本概念
    粒子效果通常基于 粒子系统 (Particle System) 实现。一个粒子系统由以下几个核心概念组成:
    ▮▮▮▮ⓐ 粒子 (Particles):粒子系统中的基本单元,可以是点、线、矩形、图像等。每个粒子具有位置、速度、加速度、颜色、大小、生命周期等属性。
    ▮▮▮▮ⓑ 发射器 (Emitters):粒子发射器负责生成粒子。发射器定义了粒子的生成速率、发射方向、初始速度、初始属性范围等。
    ▮▮▮▮ⓒ 修改器 (Modifiers):粒子修改器用于修改粒子的属性,例如位置、速度、颜色、大小等。修改器可以实现粒子的重力、风力、碰撞、颜色渐变、大小变化等效果。
    ▮▮▮▮ⓓ 渲染器 (Renderers):粒子渲染器负责将粒子绘制到屏幕上。Qt Quick 提供了多种粒子渲染器,例如 PointParticle (点粒子)、RectParticle (矩形粒子)、ImageParticle (图像粒子) 等。

    Qt Quick 粒子效果模块
    Qt Quick 提供了 QtQuick.Particles.2 模块,用于创建和管理粒子效果。该模块包含以下主要的元素:
    ▮▮▮▮ⓐ ParticleSystem 元素:粒子系统的根元素,用于组织和管理粒子效果。
    ▮▮▮▮ⓑ Emitter 元素:粒子发射器,用于生成粒子。
    ▮▮▮▮ⓒ 修改器元素 (Modifier Elements):例如 Age (生命周期修改器)、Acceleration (加速度修改器)、Affector (通用修改器)、Friction (摩擦力修改器)、Gravity (重力修改器)、Turbulence (湍流修改器)、Wander (随机游走修改器) 等,用于修改粒子的属性。
    ▮▮▮▮ⓓ 渲染器元素 (Renderer Elements):例如 PointParticleRectParticleImageParticle,用于绘制粒子。
    ▮▮▮▮ⓔ ParticlePainter 元素:抽象基类,用于自定义粒子渲染器。
    ▮▮▮▮ⓕ SpriteParticle 元素:高级粒子,可以使用精灵动画作为粒子图像。

    创建简单的火焰粒子效果
    以下代码创建一个简单的火焰粒子效果:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 import QtQuick
    2 import QtQuick.Particles.2
    3
    4 Rectangle {
    5 width: 200
    6 height: 200
    7 color: "black"
    8
    9 ParticleSystem {
    10 id: fireSystem
    11 }
    12
    13 Emitter { // 粒子发射器
    14 system: fireSystem
    15 anchors.centerIn: parent
    16 width: 50; height: 10
    17 emitRatePerSecond: 500 // 每秒发射 500 个粒子
    18 lifeSpan: 800 // 粒子生命周期 800 毫秒
    19 lifeSpanVariation: 200 // 生命周期变化范围 ±200 毫秒
    20 velocity: AngleDirection { angle: 270; angleVariation: 30; magnitude: 80; magnitudeVariation: 20 } // 粒子速度
    21 size: 8 // 粒子大小
    22 sizeVariation: 4 // 大小变化范围 ±4 像素
    23 endSize: 2 // 粒子结束大小
    24 color: "orange" // 粒子颜色
    25 colorVariation: "yellow" // 颜色变化范围 黄色
    26 alpha: 0.8 // 粒子初始透明度
    27 endAlpha: 0.0 // 粒子结束透明度
    28 }
    29
    30 Age { // 生命周期修改器
    31 system: fireSystem
    32 once: false
    33 }
    34
    35 Acceleration { // 加速度修改器,模拟火焰上升效果
    36 system: fireSystem
    37 y: -200 // 向上加速度 -200 像素/秒²
    38 }
    39
    40 PointParticle { // 点粒子渲染器
    41 system: fireSystem
    42 color: "white" // 点粒子颜色
    43 size: 2 // 点粒子大小
    44 }
    45 }

    这段代码创建了一个简单的火焰粒子效果。Emitter 元素定义了粒子的发射属性,AgeAcceleration 修改器分别控制粒子的生命周期和加速度,PointParticle 渲染器将粒子绘制为白色点。

    使用图像粒子和精灵动画
    可以使用 ImageParticle 渲染器将图像作为粒子,或者使用 SpriteParticle 渲染器和精灵动画创建更复杂的粒子效果。例如,使用 ImageParticle 创建星尘粒子效果:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 ImageParticle {
    2 system: starDustSystem
    3 source: "images/star.png" // 星星图像
    4 color: "white"
    5 alpha: 0.8
    6 rotation: 360
    7 rotationVariation: 360
    8 scale: 0.5
    9 scaleVariation: 0.2
    10 }

    使用 SpriteParticle 和精灵动画创建爆炸粒子效果:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 SpriteParticle {
    2 system: explosionSystem
    3 frameCount: 16 // 精灵动画帧数
    4 frameDuration: 50 // 每帧持续时间 50 毫秒
    5 source: "images/explosion.png" // 精灵动画图像
    6 alpha: 1.0
    7 endAlpha: 0.0
    8 scale: 1.0
    9 endScale: 2.0
    10 }

    粒子效果的优势在于:
    ▮▮▮▮ⓐ 炫酷的视觉效果 (Visually Appealing Effects):粒子效果可以创建出各种炫酷、动态的视觉效果,提升 UI 的视觉吸引力。
    ▮▮▮▮ⓑ 高性能 (High Performance):Qt Quick 粒子效果模块经过优化,可以高效地渲染大量粒子,保持良好的性能。
    ▮▮▮▮ⓒ 可定制性 (Customizable):粒子效果的各种属性和行为都可以定制,可以创建出各种独特的视觉效果。

    粒子效果通常用于创建 UI 的装饰性视觉元素,例如背景动画、过渡动画、特效动画、游戏特效等。合理使用粒子效果可以显著提升 UI 的用户体验和视觉质量。

    11.5 C++ 与 QML 交互

    11.5.1 将 C++ 数据暴露给 QML:属性、信号、槽

    在 Qt Quick 应用的 QML 前端与 C++ 后端分离架构中,C++ 数据暴露给 QML (Exposing C++ Data to QML) 是实现前端与后端交互的关键步骤。C++ 可以通过 属性 (Properties)信号 (Signals)槽 (Slots) 等机制将数据和功能暴露给 QML,使得 QML 代码可以访问和操作 C++ 对象,实现前端与后端的协同工作。

    属性暴露 (Property Exposure)
    C++ 类可以将成员变量或计算属性暴露为 QML 属性 (QML Properties),使得 QML 代码可以直接读取和修改这些属性的值。C++ 属性暴露通常通过 Q_PROPERTY 宏 (Q_PROPERTY Macro) 实现。Q_PROPERTY 宏定义了属性的名称、类型、读方法 (READ)、写方法 (WRITE) 和通知信号 (NOTIFY)。

    例如,在 C++ 类 BackendObject 中暴露一个整型属性 count

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QObject>
    2 #include <QDebug>
    3
    4 class BackendObject : public QObject {
    5 Q_OBJECT
    6 Q_PROPERTY(int count READ getCount WRITE setCount NOTIFY countChanged) // 暴露 QML 属性 count
    7
    8 public:
    9 BackendObject(QObject *parent = nullptr) : QObject(parent), m_count(0) {}
    10 ~BackendObject() override = default;
    11
    12 int getCount() const { return m_count; } // 读方法
    13 void setCount(int count) { // 写方法
    14 if (m_count != count) {
    15 m_count = count;
    16 emit countChanged(); // 发射通知信号
    17 }
    18 }
    19
    20 signals:
    21 void countChanged(); // 通知信号
    22
    23 private:
    24 int m_count;
    25 };

    在 QML 代码中,可以像访问普通属性一样访问 BackendObject 实例的 count 属性:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 import QtQuick
    2
    3 Rectangle {
    4 width: 200
    5 height: 100
    6 color: "white"
    7
    8 property var backend: backendObject // backendObject 是 C++ 暴露的上下文对象
    9
    10 Text {
    11 anchors.centerIn: parent
    12 text: "Count: " + backend.count // 访问 C++ 属性 count
    13 font.pixelSize: 24
    14 }
    15
    16 MouseArea {
    17 anchors.fill: parent
    18 onClicked: {
    19 backend.count++ // 修改 C++ 属性 count
    20 console.log("Count incremented to: " + backend.count)
    21 }
    22 }
    23 }

    在这个例子中,QML 代码可以直接读取和修改 C++ 对象的 count 属性,当 C++ 属性值改变时,QML 界面会自动更新。

    信号暴露 (Signal Exposure)
    C++ 类可以将 Qt 信号 (Qt Signals) 暴露给 QML,使得 QML 代码可以连接到这些信号,并在信号发射时执行 JavaScript 代码或调用 QML 函数。C++ 信号暴露无需额外操作,只需要在 C++ 类中定义信号即可。

    例如,在 C++ 类 BackendObject 中定义一个信号 dataUpdated

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class BackendObject : public QObject {
    2 Q_OBJECT
    3
    4 signals:
    5 void dataUpdated(const QString& newData); // 定义信号 dataUpdated,携带字符串参数
    6
    7 public:
    8 // ...
    9
    10 void updateData(const QString& data) {
    11 emit dataUpdated(data); // 发射信号
    12 }
    13 };

    在 QML 代码中,可以连接到 BackendObject 实例的 dataUpdated 信号,并在信号发射时执行 JavaScript 代码:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 import QtQuick
    2
    3 Rectangle {
    4 width: 200
    5 height: 100
    6 color: "white"
    7
    8 property var backend: backendObject
    9
    10 Component.onCompleted: {
    11 backend.dataUpdated.connect(function(newData) { // 连接 C++ 信号 dataUpdated
    12 console.log("Data updated from C++ backend: " + newData);
    13 // ... 处理新数据 ...
    14 });
    15 }
    16
    17 MouseArea {
    18 anchors.fill: parent
    19 onClicked: {
    20 backend.updateData("New Data from QML"); // 调用 C++ 方法,触发信号发射
    21 }
    22 }
    23 }

    在这个例子中,QML 代码连接到 C++ 对象的 dataUpdated 信号,当 C++ 后端发射 dataUpdated 信号时,QML 代码中的 JavaScript 槽函数会被自动调用,处理信号携带的数据。

    槽暴露 (Slot Exposure)
    C++ 类可以将 Qt 槽函数 (Qt Slots) 暴露给 QML,使得 QML 代码可以调用这些槽函数,执行 C++ 后端的功能。C++ 槽函数暴露无需额外操作,只需要在 C++ 类中定义槽函数即可。

    例如,在 C++ 类 BackendObject 中定义一个槽函数 processData

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class BackendObject : public QObject {
    2 Q_OBJECT
    3
    4 public slots:
    5 void processData(const QString& data) { // 定义槽函数 processData
    6 qDebug() << "Processing data from QML: " << data;
    7 // ... 处理数据逻辑 ...
    8 }
    9
    10 public:
    11 // ...
    12 };

    在 QML 代码中,可以调用 BackendObject 实例的 processData 槽函数:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 import QtQuick
    2
    3 Rectangle {
    4 width: 200
    5 height: 100
    6 color: "white"
    7
    8 property var backend: backendObject
    9
    10 Button {
    11 text: "Process Data"
    12 anchors.centerIn: parent
    13 onClicked: {
    14 backend.processData("Data to be processed"); // 调用 C++ 槽函数 processData
    15 }
    16 }
    17 }

    在这个例子中,QML 代码可以直接调用 C++ 对象的 processData 槽函数,执行 C++ 后端的数据处理逻辑。

    通过属性、信号和槽的暴露,C++ 后端可以将数据、状态和功能有效地传递给 QML 前端,实现前端与后端的双向通信和协同工作。这种机制使得 Qt Quick 应用能够充分利用 C++ 的高性能和系统编程能力,同时保持 QML 前端的灵活性和易用性。

    11.5.2 在 C++ 中调用 QML 函数与访问 QML 对象

    除了 QML 调用 C++ 的功能外,C++ 也可以调用 QML 函数 (Calling QML Functions from C++)访问 QML 对象 (Accessing QML Objects from C++),实现双向交互。C++ 调用 QML 和访问 QML 对象通常通过 QQmlEngine (QQmlEngine Class)QObject::findChild (QObject::findChild Function) 等 API 实现。

    在 C++ 中调用 QML 函数
    C++ 可以通过 QQmlEngineQMetaObject::invokeMethod 等 API 调用在 QML 中定义的 JavaScript 函数。要调用 QML 函数,首先需要获取 QML 对象的指针 (Pointer to QML Object),然后使用 QQmlEngine::invokeMethod 调用 QML 函数。

    例如,假设在 QML 文件 MyComponent.qml 中定义了一个名为 qmlFunction 的 JavaScript 函数:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // MyComponent.qml
    2 import QtQuick
    3
    4 Rectangle {
    5 id: root
    6 width: 200
    7 height: 100
    8 color: "white"
    9
    10 function qmlFunction(message) { // 定义 QML 函数 qmlFunction
    11 console.log("QML Function called with message: " + message);
    12 return "Response from QML";
    13 }
    14 }

    在 C++ 代码中,加载 MyComponent.qml 组件,获取根对象指针,并调用 qmlFunction 函数:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QGuiApplication>
    2 #include <QQmlApplicationEngine>
    3 #include <QQuickItem>
    4 #include <QDebug>
    5
    6 int main(int argc, char *argv[]) {
    7 QGuiApplication app(argc, argv);
    8
    9 QQmlApplicationEngine engine;
    10 engine.load(QUrl(QStringLiteral("qrc:/MyComponent.qml")));
    11 if (engine.rootObjects().isEmpty())
    12 return -1;
    13
    14 QObject *rootObject = engine.rootObjects().first(); // 获取根对象指针
    15
    16 QVariant returnValue;
    17 QMetaObject::invokeMethod(rootObject, "qmlFunction", // 调用 QML 函数 qmlFunction
    18 Q_RETURN_ARG(QVariant, returnValue),
    19 Q_ARG(QVariant, "Hello from C++")); // 传递参数 "Hello from C++"
    20
    21 qDebug() << "Return value from QML function: " << returnValue.toString(); // 获取 QML 函数返回值
    22
    23 return app.exec();
    24 }

    这段 C++ 代码首先加载 QML 组件,获取根对象指针 rootObject,然后使用 QMetaObject::invokeMethod 调用 rootObjectqmlFunction 函数,并传递参数 "Hello from C++"invokeMethod 函数会执行 QML 函数,并将返回值存储在 returnValue 变量中。

    在 C++ 中访问 QML 对象
    C++ 可以通过 QObject::findChildQObject::findChildren 等 API 查找和访问 QML 文件中定义的 QML 对象 (QML Objects)。要访问 QML 对象,首先需要获取 根对象指针 (Root Object Pointer),然后使用 findChildfindChildren 在对象树中查找目标对象。

    例如,假设在 QML 文件 MyComponent.qml 中定义了一个 ID 为 myTextText 元素:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // MyComponent.qml
    2 import QtQuick
    3
    4 Rectangle {
    5 id: root
    6 width: 200
    7 height: 100
    8 color: "white"
    9
    10 Text {
    11 id: myText // 定义 ID 为 myText 的 Text 元素
    12 text: "Initial Text"
    13 font.pixelSize: 20
    14 anchors.centerIn: parent
    15 }
    16 }

    在 C++ 代码中,加载 MyComponent.qml 组件,获取根对象指针,并查找和访问 ID 为 myTextText 元素,修改其 text 属性:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QGuiApplication>
    2 #include <QQmlApplicationEngine>
    3 #include <QQuickItem>
    4 #include <QDebug>
    5 #include <QQuickTextDocument>
    6
    7 int main(int argc, char *argv[]) {
    8 QGuiApplication app(argc, argv);
    9
    10 QQmlApplicationEngine engine;
    11 engine.load(QUrl(QStringLiteral("qrc:/MyComponent.qml")));
    12 if (engine.rootObjects().isEmpty())
    13 return -1;
    14
    15 QObject *rootObject = engine.rootObjects().first(); // 获取根对象指针
    16
    17 QObject *textObject = rootObject->findChild<QObject*>("myText"); // 查找 ID 为 myText 的对象
    18 if (textObject) {
    19 QVariant textValue = "Text updated from C++";
    20 textObject->setProperty("text", textValue); // 修改 text 属性
    21 qDebug() << "Text property updated in QML: " << textValue.toString();
    22 } else {
    23 qWarning() << "QML Text object with ID 'myText' not found!";
    24 }
    25
    26 return app.exec();
    27 }

    这段 C++ 代码首先加载 QML 组件,获取根对象指针 rootObject,然后使用 rootObject->findChild<QObject*>("myText") 查找 ID 为 myText 的对象。如果找到该对象,则使用 setProperty("text", textValue) 修改其 text 属性。

    C++ 调用 QML 函数和访问 QML 对象为 C++ 后端提供了 反向控制 QML 前端 (Reverse Control of QML Frontend) 的能力。C++ 可以根据业务逻辑动态地调用 QML 函数、修改 QML 属性,实现更灵活的前后端交互和 UI 控制。然而,过度依赖 C++ 反向控制 QML 可能会破坏 QML 前端与 C++ 后端的分离原则,建议谨慎使用,保持前后端职责的清晰划分。通常情况下,C++ 暴露数据和功能给 QML 使用,QML 驱动 UI 更新和用户交互,是更常见和推荐的交互模式。

    11.5.3 使用 Context Properties 和 Context Objects 传递数据

    Context Properties (上下文属性)Context Objects (上下文对象) 是 Qt Quick 中用于 在 C++ 和 QML 之间传递数据 (Passing Data between C++ and QML) 的重要机制。通过上下文属性和上下文对象,可以将 C++ 对象和数据注册到 QML 的 上下文 (Context) 中,使得 QML 代码可以直接访问这些 C++ 对象和数据,实现数据共享和传递。

    Context Properties (上下文属性)
    Context Properties (上下文属性) 是将 C++ 变量 (C++ Variables)C++ 常量 (C++ Constants) 注册到 QML 上下文中的一种方式。注册上下文属性后,QML 代码可以直接使用属性名访问 C++ 变量或常量的值。注册上下文属性通常通过 QQmlContext::setContextProperty 函数实现。

    例如,在 C++ 代码中,将一个整型变量 appVersion 注册为名为 applicationVersion 的上下文属性:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QGuiApplication>
    2 #include <QQmlApplicationEngine>
    3 #include <QQmlContext>
    4
    5 int main(int argc, char *argv[]) {
    6 QGuiApplication app(argc, argv);
    7
    8 QQmlApplicationEngine engine;
    9 int appVersion = 123;
    10 engine.rootContext()->setContextProperty("applicationVersion", appVersion); // 注册上下文属性 applicationVersion
    11
    12 engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));
    13 if (engine.rootObjects().isEmpty())
    14 return -1;
    15
    16 return app.exec();
    17 }

    在 QML 代码中,可以直接使用 applicationVersion 属性访问 C++ 注册的变量值:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // Main.qml
    2 import QtQuick
    3
    4 Rectangle {
    5 width: 200
    6 height: 100
    7 color: "white"
    8
    9 Text {
    10 anchors.centerIn: parent
    11 text: "Application Version: " + applicationVersion // 访问上下文属性 applicationVersion
    12 font.pixelSize: 20
    13 }
    14 }

    在这个例子中,QML 代码可以直接访问 C++ 注册的 applicationVersion 上下文属性,显示应用的版本号。

    Context Objects (上下文对象)
    Context Objects (上下文对象) 是将 C++ 对象实例 (C++ Object Instances) 注册到 QML 上下文中的一种方式。注册上下文对象后,QML 代码可以直接访问 C++ 对象实例的属性、调用其方法和连接其信号。注册上下文对象通常通过 QQmlContext::setContextObject 函数实现。

    例如,在 C++ 代码中,创建一个 BackendObject 实例,并注册为名为 backendObject 的上下文对象:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QGuiApplication>
    2 #include <QQmlApplicationEngine>
    3 #include <QQmlContext>
    4 #include "backendobject.h" // 假设 BackendObject 类定义在 backendobject.h 中
    5
    6 int main(int argc, char *argv[]) {
    7 QGuiApplication app(argc, argv);
    8
    9 QQmlApplicationEngine engine;
    10 BackendObject *backend = new BackendObject(&engine); // 创建 BackendObject 实例
    11 engine.rootContext()->setContextObject(backend); // 注册上下文对象 backendObject
    12
    13 engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));
    14 if (engine.rootObjects().isEmpty())
    15 return -1;
    16
    17 return app.exec();
    18 }

    在 QML 代码中,可以直接使用 backendObject 上下文对象,访问其属性、调用其方法和连接其信号:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // Main.qml
    2 import QtQuick
    3
    4 Rectangle {
    5 width: 200
    6 height: 100
    7 color: "white"
    8
    9 property var backend: backendObject // 访问上下文对象 backendObject
    10
    11 Text {
    12 anchors.centerIn: parent
    13 text: "Count: " + backend.count // 访问 C++ 对象属性 count
    14 font.pixelSize: 24
    15 }
    16
    17 MouseArea {
    18 anchors.fill: parent
    19 onClicked: {
    20 backend.setCount(backend.count + 1); // 调用 C++ 对象方法 setCount
    21 console.log("Count incremented to: " + backend.count)
    22 }
    23 }
    24
    25 Connections { // 连接 C++ 对象信号
    26 target: backend
    27 onCountChanged: { // 响应 C++ 信号 countChanged
    28 console.log("Count changed signal received from C++ backend: " + backend.count);
    29 }
    30 }
    31 }

    在这个例子中,QML 代码可以直接访问 C++ 注册的 backendObject 上下文对象,读取和修改其 count 属性,调用其 setCount 方法,并连接其 countChanged 信号。

    上下文属性和上下文对象的优势在于:
    ▮▮▮▮ⓐ 简单的数据传递 (Simple Data Passing):上下文属性和上下文对象提供了一种简单、直接的方式将 C++ 数据和对象传递给 QML 前端,无需复杂的桥接代码。
    ▮▮▮▮ⓑ 全局访问 (Global Access):注册到根上下文的属性和对象可以在整个 QML 应用中全局访问,方便数据共享和组件通信。
    ▮▮▮▮ⓒ 数据绑定 (Data Binding):QML 可以通过属性绑定直接绑定到上下文属性和上下文对象的属性,实现数据驱动的 UI 更新。

    上下文属性和上下文对象通常用于将应用配置、全局数据、后端服务对象等注册到 QML 上下文中,供整个 QML 应用使用。对于更复杂的数据模型和数据管理,可以使用 模型/视图 (Model/View) 架构和 数据模型 (Data Models) 等更高级的机制。

    12. 测试与调试:保障程序质量

    12.1 单元测试 (Unit Testing) 基础

    12.1.1 什么是单元测试?

    单元测试 (Unit Testing) 是一种软件测试方法,旨在验证软件中最小可测试单元的行为是否符合预期。这里的“单元 (unit)”通常指的是函数 (function)方法 (method)类 (class)模块 (module) 等。单元测试的核心思想是将程序分解为小的、独立的部分进行测试,从而隔离错误,并确保每个单元在独立工作时是正确的。

    单元测试的目的 主要包括:

    尽早发现缺陷 (defect):在开发周期的早期,单元测试可以帮助开发者快速发现代码中的错误。相比于集成测试或系统测试,单元测试的范围更小,更容易定位和修复问题。

    提高代码质量 (code quality):编写单元测试迫使开发者更清晰地思考代码的设计和实现,从而写出更健壮、更可靠的代码。

    支持代码重构 (code refactoring):当需要对代码进行重构时,完善的单元测试可以作为安全网 (safety net),确保重构后的代码功能与之前保持一致,避免引入新的错误。

    提升开发效率 (development efficiency):虽然编写单元测试需要额外的时间,但从长远来看,它可以减少调试时间,提高代码的可维护性,从而提升整体的开发效率。

    作为文档 (documentation):单元测试用例本身也可以作为代码的文档,清晰地展示了每个单元的功能和预期行为,帮助其他开发者理解代码。

    与传统测试方法的区别

    传统的软件测试方法,如集成测试 (Integration Testing) 和系统测试 (System Testing),通常关注整个系统或模块的整体功能。而单元测试则更加细粒度,专注于代码的局部功能验证。

    特性 (Feature)单元测试 (Unit Testing)集成测试/系统测试 (Integration/System Testing)
    测试范围 (Scope)最小单元 (函数、方法、类)模块、子系统、整个系统
    测试目的 (Purpose)验证单元的独立功能是否正确验证模块间交互、系统整体功能是否正确
    测试时间 (Timing)开发早期 (编码阶段)开发后期 (集成阶段、系统测试阶段)
    执行者 (Tester)开发者 (Developer)测试工程师 (Test Engineer)
    反馈速度 (Feedback)快 (快速定位错误)慢 (定位错误较复杂)
    自动化程度 (Automation)高 (易于自动化)相对较低 (部分自动化)

    总而言之,单元测试是保证代码质量提高开发效率的重要手段,是现代软件开发中不可或缺的环节。通过编写和执行单元测试,开发者可以更加自信地交付高质量的软件产品。

    12.1.2 单元测试的优势:尽早发现 Bug, 代码重构保障

    单元测试在软件开发过程中扮演着至关重要的角色,其优势主要体现在以下几个方面:

    尽早发现 Bug (defect)

    编码阶段测试:单元测试通常在编码完成后立即进行,这意味着 Bug 可以在开发的早期阶段就被发现和修复。相较于在集成测试或系统测试阶段才发现 Bug,单元测试的 反馈周期更短,修复成本也更低。
    精确定位错误:由于单元测试针对的是代码的最小单元,当测试失败时,可以快速定位到具体的代码行,减少了调试时间,提高了问题解决效率。
    减少 Bug 累积:早期发现和修复 Bug 可以防止 Bug 在系统中累积,避免在后期形成更复杂、更难以解决的问题。

    代码重构 (code refactoring) 保障

    安全网 (safety net):代码重构是在不改变软件外部行为的前提下,改进其内部结构的过程。单元测试为代码重构提供了 安全保障。在重构之前,先编写充分的单元测试用例。重构之后,再次运行这些单元测试,如果所有测试都通过,则可以 确信重构没有破坏原有的功能
    持续改进代码:单元测试的存在使得开发者可以 更放心地进行代码改进和优化,而不用担心引入新的错误。这鼓励了代码的持续演进和质量提升。
    促进代码理解:为了编写有效的单元测试,开发者需要深入理解代码的逻辑和功能。这个过程本身 促进了开发者对代码的理解,为代码重构打下基础。

    提高代码质量 (code quality)

    驱动设计 (design driving):在某些开发模式(如测试驱动开发 (Test-Driven Development, TDD))中,单元测试甚至先于代码编写。这种模式 迫使开发者在编码之前先思考代码的设计,从而产生更清晰、更模块化的代码结构。
    促进良好编程习惯:为了使代码更易于测试,开发者会 倾向于编写更简洁、更低耦合的代码。这有助于提高代码的可读性、可维护性和可复用性。
    减少技术债务 (technical debt):通过持续的单元测试,可以 及时发现和修复代码中的潜在问题,减少技术债务的积累,保持代码库的健康状态。

    提升开发效率 (development efficiency)

    减少调试时间:虽然编写单元测试需要时间,但它可以 显著减少后期的调试时间。早期发现的 Bug 往往更容易修复,避免了在后期花费大量时间进行复杂调试。
    提高代码信心:完善的单元测试可以让开发者 对代码的质量更有信心,从而更快速、更高效地进行开发工作。
    支持持续集成 (Continuous Integration, CI):单元测试是持续集成流程中的重要组成部分。通过自动化执行单元测试,可以 快速反馈代码变更是否引入错误,保证代码库的稳定性和可靠性,从而加速开发迭代周期。

    综上所述,单元测试不仅可以帮助 尽早发现和修复 Bug,还可以 保障代码重构的安全性提高代码质量,并最终 提升软件开发的效率。对于任何规模的软件项目,单元测试都是一项非常有价值的实践。

    12.1.3 单元测试框架:Qt Test

    为了更方便地编写和执行单元测试,通常会使用专门的 单元测试框架 (Unit Testing Framework)。对于 C++ 和 Qt GUI 编程,Qt Test 框架 (Qt Test Framework) 是一个非常强大且易于使用的选择。Qt Test 框架是 Qt 官方提供的测试工具,与 Qt 库本身高度集成,可以方便地测试 Qt 应用程序的各个方面,包括 GUI 组件、核心逻辑、以及与其他 Qt 模块的交互。

    Qt Test 框架的主要特点和优势

    与 Qt 库深度集成

    无缝兼容:Qt Test 框架是 Qt 生态系统的一部分,与 Qt 库的其他模块 (如 QtCore, QtGui, QtWidgets 等) 完美兼容
    Qt 类型支持:可以直接测试使用了 Qt 类型 (如 QString, QList, QMap 等) 的代码,无需额外的类型转换或适配。
    元对象系统 (Meta-Object System) 支持:可以方便地测试使用了 Qt 元对象系统特性的类,如信号 (signal) 与槽 (slot)。

    简洁易用的 API (应用程序编程接口)

    基于宏 (macro) 的断言 (assertion):Qt Test 框架提供了一系列 简洁的宏,用于编写断言,例如 QCOMPARE (比较两个值是否相等), QVERIFY (验证条件是否为真), QFAIL (强制测试失败) 等。这些宏使得测试代码 清晰易懂,易于编写和维护。
    数据驱动测试 (Data-Driven Testing) 支持:通过 QTest::addColumnQTest::addRow 等宏,可以方便地实现 数据驱动的测试,使用不同的输入数据运行相同的测试逻辑,提高测试覆盖率。
    丰富的测试功能:Qt Test 框架提供了 setup (准备)cleanup (清理) 函数,用于在每个测试用例执行前后进行必要的初始化和清理工作。还支持 benchmark (基准测试)GUI 测试 等高级功能。

    多种运行方式

    Qt Creator 集成:Qt Test 测试用例可以 无缝集成到 Qt Creator IDE (集成开发环境) 中,方便开发者在 IDE 中编写、运行和调试测试。
    命令行运行:Qt Test 测试用例也可以编译成 独立的命令行可执行程序,方便在持续集成 (CI) 系统中自动化运行测试。
    XML 输出:Qt Test 可以生成 XML 格式的测试报告,方便与各种测试报告分析工具集成。

    跨平台 (cross-platform) 支持

    与 Qt 跨平台特性一致:Qt Test 框架本身也是 跨平台的,可以在 Windows, macOS, Linux 等多个操作系统上运行,保证了测试的一致性。

    Qt Test 框架的基本组成部分

    测试类 (Test Class):包含一组测试用例的类,通常 继承自 QObject,并使用 Q_OBJECT 宏启用 Qt 的元对象特性。测试类中的每个测试用例都是一个 public slots (公共槽) 函数。
    测试用例 (Test Case):测试类中的 public slots 函数,用于编写具体的测试逻辑。测试用例函数的名字通常以 test 开头,例如 testAdd, testRemove 等。
    断言宏 (Assertion Macros):用于在测试用例中 验证预期结果 的宏,例如 QCOMPARE(actual, expected), QVERIFY(condition), QFAIL(message) 等。
    数据驱动宏 (Data-Driven Macros):用于实现数据驱动测试的宏,例如 QTest::addColumn, QTest::addRow 等。
    setup 和 cleanup 函数:可选的 setup 函数 (在每个测试用例执行前调用) 和 cleanup 函数 (在每个测试用例执行后调用),用于进行测试环境的初始化和清理。

    总结

    Qt Test 框架是一个 强大、易用且与 Qt 库深度集成 的单元测试框架。它提供了丰富的功能和灵活的运行方式,可以帮助 Qt 开发者有效地编写和执行单元测试,保证 Qt GUI 应用程序的质量和稳定性。在后续的章节中,将详细介绍 Qt Test 框架的使用方法和最佳实践。

    12.2 Qt Test 框架使用

    12.2.1 创建测试类:继承 QObject, 添加 Q_OBJECT 宏

    要使用 Qt Test 框架编写单元测试,首先需要创建一个 测试类 (Test Class)。测试类是包含一组测试用例的类,它需要遵循一定的规范才能被 Qt Test 框架识别和执行。

    创建测试类的基本步骤

    继承 QObject

    ⚝ 测试类必须 继承自 QObjectQObject 是 Qt 元对象系统 (Meta-Object System) 的基础类,提供了信号 (signal) 与槽 (slot)、属性系统 (property system) 等特性。Qt Test 框架依赖于 Qt 的元对象系统来识别和执行测试用例。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QObject>
    2 #include <QtTest> // 包含 QtTest 头文件
    3
    4 class MyTest : public QObject // 继承 QObject
    5 {
    6 Q_OBJECT // 添加 Q_OBJECT 宏
    7
    8 public:
    9 MyTest(QObject *parent = nullptr) : QObject(parent) {}
    10
    11 private slots: // 测试用例需要声明为 private slots
    12 void testCase1();
    13 void testCase2();
    14 // ... 更多测试用例
    15 };

    添加 Q_OBJECT

    ⚝ 在测试类的声明中,必须 添加 Q_OBJECTQ_OBJECT 宏是 Qt 元对象系统的关键,它会 触发 moc (Meta-Object Compiler, 元对象编译器) 对该类进行预处理,生成元对象代码。这些元对象代码是 Qt Test 框架识别测试类和测试用例的基础。
    ⚝ 如果忘记添加 Q_OBJECT 宏,Qt Test 框架将 无法识别该类为测试类,测试用例也无法被执行。

    声明测试用例为 private slots

    ⚝ 测试类中的 测试用例函数 (Test Case Function) 必须声明为 private slots。虽然从 C++ 语法上来说,public slots 也可以工作,但 最佳实践是将测试用例声明为 private slots
    slots 关键字是 Qt 元对象系统的一部分,表示这些函数是 槽函数,可以被信号 (signal) 连接和调用。Qt Test 框架通过元对象系统 反射 (reflection) 测试类,并查找 private slots 中的函数作为测试用例。

    实现测试用例函数

    ⚝ 在测试类的源文件 (.cpp) 中,需要 实现声明的测试用例函数。测试用例函数中编写具体的测试逻辑,并使用 Qt Test 框架提供的 断言宏 (assertion macros) 来验证预期结果。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include "mytest.h" // 包含测试类头文件
    2 #include <QString>
    3
    4 void MyTest::testCase1()
    5 {
    6 QString str = "hello";
    7 QCOMPARE(str.length(), 5); // 使用 QCOMPARE 断言字符串长度是否为 5
    8 }
    9
    10 void MyTest::testCase2()
    11 {
    12 int a = 10;
    13 int b = 20;
    14 QVERIFY(a < b); // 使用 QVERIFY 断言 a 是否小于 b
    15 }
    16
    17 // ... 更多测试用例实现

    注册测试类并运行

    ⚝ 在 main 函数中,需要 创建测试类的实例,并调用 QTest::qExec 函数来 执行测试QTest::qExec 函数会 自动发现和执行测试类中的所有测试用例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include "mytest.h" // 包含测试类头文件
    2 #include <QApplication>
    3 #include <QtTest>
    4
    5 int main(int argc, char *argv[])
    6 {
    7 QApplication app(argc, argv); // GUI 应用程序需要 QApplication 实例
    8 MyTest testObj;
    9 return QTest::qExec(&testObj, argc, argv); // 执行测试并返回结果
    10 }

    总结

    创建 Qt Test 测试类需要 继承 QObject添加 Q_OBJECT,并将 测试用例函数声明为 private slots。在测试用例函数中编写测试逻辑,并使用 Qt Test 框架提供的断言宏进行结果验证。最后,在 main 函数中 注册测试类并调用 QTest::qExec 函数 来执行测试。遵循这些步骤,就可以开始使用 Qt Test 框架编写单元测试了。

    12.2.2 编写测试函数:使用 QTest::addColumn, QCOMPARE, QVERIFY 等宏

    编写测试函数 (Test Function) 是单元测试的核心环节。在 Qt Test 框架中,测试函数是测试类中声明为 private slots 的函数,用于 编写具体的测试逻辑验证预期结果。Qt Test 框架提供了一系列 宏 (macros),用于简化测试函数的编写,特别是 断言宏 (assertion macros),可以方便地进行结果验证。

    常用的 Qt Test 断言宏

    QCOMPARE(actual, expected):

    功能:比较 actual (实际值) 和 expected (期望值) 是否 相等。如果相等,测试通过;否则,测试失败,并输出错误信息,包含实际值和期望值。
    适用类型:可以比较 各种基本类型 (int, float, QString 等)Qt 类型
    示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyTest::testStringLength()
    2 {
    3 QString str = "hello";
    4 QCOMPARE(str.length(), 5); // 断言字符串长度是否为 5
    5 }
    6
    7 void MyTest::testAddition()
    8 {
    9 int a = 10;
    10 int b = 20;
    11 QCOMPARE(a + b, 30); // 断言加法结果是否为 30
    12 }

    QVERIFY(condition):

    功能:验证 condition (条件) 是否为 真 (true)。如果为真,测试通过;否则,测试失败,并输出错误信息,包含失败的条件表达式。
    适用场景:适用于 验证布尔条件复杂逻辑判断 的结果。
    示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyTest::testLessThan()
    2 {
    3 int a = 10;
    4 int b = 20;
    5 QVERIFY(a < b); // 断言 a 是否小于 b
    6 }
    7
    8 void MyTest::testStringContains()
    9 {
    10 QString str = "hello world";
    11 QVERIFY(str.contains("world")); // 断言字符串是否包含 "world"
    12 }

    QFAIL(message):

    功能强制测试失败,并输出指定的 message (错误信息)。
    适用场景:在测试过程中,如果遇到 不应该发生的情况无法继续测试的错误,可以使用 QFAIL 强制测试失败,并提供相应的错误信息。
    示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyTest::testFileOpen()
    2 {
    3 QFile file("non_existent_file.txt");
    4 if (!file.open(QIODevice::ReadOnly)) {
    5 QFAIL("Failed to open file!"); // 文件打开失败,强制测试失败
    6 return; // 提前返回,避免后续代码执行
    7 }
    8 // ... 后续文件操作
    9 file.close();
    10 }

    其他常用断言宏:

    QVERIFY2(condition, message): 与 QVERIFY 类似,但可以 自定义错误信息 message
    QEXPECT_FAIL(testCase, reason, mode): 预期测试失败 的宏。如果测试用例 testCase 确实失败,则测试结果为 "Passed with failures" (通过但有失败);如果测试用例意外地通过了,则测试结果为 "Failure" (失败)。mode 可以指定失败的模式 (如 xfail - 预期失败, skip - 跳过)。
    QWARN(message)QINFO(message): 输出警告信息和信息性消息,但不会导致测试失败。通常用于在测试过程中输出一些辅助信息。

    数据驱动测试 (Data-Driven Testing) 的宏:

    Qt Test 框架支持 数据驱动测试 (Data-Driven Testing),可以使用相同的测试逻辑,但 使用不同的输入数据进行多次测试,从而提高测试覆盖率。数据驱动测试主要使用以下宏:

    QTest::addColumn<DataType>("columnName"):

    功能:在测试类中 声明一个数据列,用于存储测试数据。DataType 是数据类型,"columnName" 是列名。需要在测试类中 静态地声明数据列
    示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyTestDataDrivenTest : public QObject
    2 {
    3 Q_OBJECT
    4
    5 private slots:
    6 void testAddition_data(); // 数据提供函数
    7 void testAddition(); // 测试用例函数
    8
    9 private:
    10 Q_DECLARE_METADATA // 声明元数据
    11 };
    12
    13 void MyTestDataDrivenTest::testAddition_data()
    14 {
    15 QTest::addColumn<int>("a"); // 声明 int 类型的列 "a"
    16 QTest::addColumn<int>("b"); // 声明 int 类型的列 "b"
    17 QTest::addColumn<int>("sum"); // 声明 int 类型的列 "sum"
    18
    19 QTest::addRow("positive numbers") << 1 << 2 << 3; // 添加一行数据,列名为 "positive numbers"
    20 QTest::addRow("negative numbers") << -1 << -2 << -3; // 添加一行数据,列名为 "negative numbers"
    21 QTest::addRow("zero and positive") << 0 << 5 << 5; // 添加一行数据,列名为 "zero and positive"
    22 }

    QTest::addRow("rowName") << data1 << data2 << ...:

    功能:在数据提供函数中 添加一行测试数据"rowName" 是行名 (用于标识数据行),data1, data2, ... 是 QTest::addColumn 声明的列对应的数据值,需要 按照列的顺序提供数据

    在测试用例函数中使用数据:

    ⚝ 在测试用例函数中,可以使用 QTest::currentData() 函数 获取当前测试数据行的数据,并 通过列名访问数据

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyTestDataDrivenTest::testAddition()
    2 {
    3 QFETCH(int, a); // 获取列 "a" 的数据
    4 QFETCH(int, b); // 获取列 "b" 的数据
    5 QFETCH(int, sum); // 获取列 "sum" 的数据
    6
    7 QCOMPARE(a + b, sum); // 使用获取的数据进行测试
    8 }

    测试函数的命名规范:

    ⚝ 测试函数的名字通常以 test 开头,后面跟上被测试的功能或场景的描述,例如 testStringLength, testAddition, testFileOpen 等。
    ⚝ 对于数据驱动测试,数据提供函数的名字通常是被测试函数的名字后面加上 _data 后缀,例如 testAddition_data 对应于 testAddition 测试用例函数。

    总结

    编写测试函数时,需要 根据测试目标选择合适的断言宏,例如 QCOMPARE 用于比较相等性,QVERIFY 用于验证条件真假,QFAIL 用于强制测试失败。对于需要使用多组数据进行测试的场景,可以使用 数据驱动测试,通过 QTest::addColumnQTest::addRow 等宏来组织和提供测试数据。遵循良好的测试函数命名规范,可以提高测试代码的可读性和可维护性。

    12.2.3 运行测试用例:Qt Creator 集成、命令行运行

    编写完 Qt Test 测试用例后,需要 运行这些测试用例 并查看测试结果。Qt Test 框架提供了 多种运行测试用例的方式,包括 Qt Creator IDE 集成运行和命令行运行。

    ① Qt Creator 集成运行:

    Qt Creator IDE 对 Qt Test 框架提供了 良好的集成支持,可以直接在 IDE 中方便地运行和管理测试用例。

    创建测试项目:
    ▮▮▮▮⚝ 在 Qt Creator 中,可以创建 "Qt Test Project" (Qt 测试项目) 类型的项目。这种项目模板已经 预配置好了 Qt Test 框架,并包含了运行测试的基本设置。
    ▮▮▮▮⚝ 如果已经有现有的 Qt 项目,也可以 手动添加 Qt Test 支持,需要在项目的 .pro 文件中 添加 QT += testlib 模块,并配置测试相关的构建步骤。

    构建测试项目:
    ▮▮▮▮⚝ 像构建普通的 Qt 项目一样,点击 Qt Creator 的 构建按钮 (Build) 或使用快捷键 (如 Ctrl+B 或 Cmd+B) 构建测试项目。Qt Creator 会 自动编译测试类和测试用例

    运行测试用例:
    ▮▮▮▮⚝ 构建成功后,在 Qt Creator 的 侧边栏 (Sidebar) 中,切换到 "Projects" (项目) 视图。
    ▮▮▮▮⚝ 在项目视图中,展开 "Build" (构建) 目录,找到 测试可执行文件 (通常与项目名相同)
    ▮▮▮▮⚝ 右键点击测试可执行文件,选择 "Run Tests" (运行测试)。Qt Creator 会 自动运行测试用例,并在 "Test Results" (测试结果) 窗口中显示测试结果。

    查看测试结果:
    ▮▮▮▮⚝ "Test Results" (测试结果) 窗口会显示 测试用例的执行状态 (Passed, Failed, Inconclusive 等)运行时间错误信息 等。
    ▮▮▮▮⚝ 可以 点击测试用例 查看详细的测试输出,包括断言失败的信息、警告信息、信息性消息等。
    ▮▮▮▮⚝ Qt Creator 还提供了 图形化的测试结果报告,方便开发者快速了解测试的整体情况。

    调试测试用例:
    ▮▮▮▮⚝ 如果测试用例失败,可以在 Qt Creator 中 设置断点 (breakpoint),然后 以调试模式 (Debug Mode) 运行测试用例。Qt Creator 会 启动调试器 (debugger),允许开发者 单步调试测试代码,查看变量值,定位错误原因。

    ② 命令行运行:

    Qt Test 测试用例也可以编译成 独立的命令行可执行程序,方便在命令行或脚本中运行测试。

    构建测试项目:
    ▮▮▮▮⚝ 同样需要先 构建测试项目,生成测试可执行文件。

    命令行运行测试:
    ▮▮▮▮⚝ 打开 命令行终端 (Command Line Terminal)控制台 (Console)
    ▮▮▮▮⚝ 切换到测试可执行文件所在的目录 (通常在构建目录下的 debugrelease 目录中)。
    ▮▮▮▮⚝ 直接运行测试可执行文件 (例如,在 Linux/macOS 上运行 ./mytest,在 Windows 上运行 mytest.exe)。

    查看命令行测试结果:
    ▮▮▮▮⚝ 测试结果会 输出到命令行终端,包括 测试用例的执行状态错误信息警告信息 等。
    ▮▮▮▮⚝ 命令行输出的格式 相对简洁,主要用于 快速查看测试结果

    命令行参数:
    ▮▮▮▮⚝ Qt Test 命令行可执行程序支持一些 命令行参数,用于控制测试的运行行为,例如:
    ▮▮▮▮▮▮▮▮⚝ -o <output_file.xml>: 将测试结果 输出到 XML 文件
    ▮▮▮▮▮▮▮▮⚝ -v <verbosity_level>: 设置 输出详细程度 (verbosity level)。
    ▮▮▮▮▮▮▮▮⚝ -testcase <test_case_name>: 只运行指定的测试用例
    ▮▮▮▮▮▮▮▮⚝ -gui: 启用 GUI 测试 (用于测试 GUI 组件)。
    ▮▮▮▮▮▮▮▮⚝ -no-gui: 禁用 GUI 测试 (即使测试用例中包含了 GUI 代码)。
    ▮▮▮▮▮▮▮▮⚝ -help-h: 显示 帮助信息,列出所有可用的命令行参数。

    自动化测试与持续集成 (CI):
    ▮▮▮▮⚝ 命令行运行方式 非常适合用于自动化测试和持续集成 (CI) 系统。可以将测试命令集成到 CI 脚本中,每次代码提交或构建时自动运行测试,并生成测试报告,及时发现和反馈代码变更引入的错误。
    ▮▮▮▮⚝ 可以结合 XML 输出参数 (-o),将测试结果输出为 XML 文件,然后使用 测试报告分析工具 (如 Jenkins, JUnit 等) 解析 XML 文件,生成更丰富的测试报告和统计信息。

    选择运行方式:

    开发阶段: 在 开发和调试阶段,通常 在 Qt Creator IDE 中运行测试 更方便,可以利用 IDE 的集成调试功能快速定位和修复错误。
    自动化测试: 在 自动化测试和持续集成 (CI) 流程中命令行运行 更加灵活和高效,可以方便地集成到各种自动化脚本和 CI 系统中。

    总结

    Qt Test 测试用例可以通过 Qt Creator IDE 集成运行命令行运行 两种方式执行。Qt Creator 集成运行提供了 图形化的界面和调试功能,适合开发和调试阶段;命令行运行更加 灵活和高效,适合自动化测试和持续集成。开发者可以根据不同的场景和需求选择合适的运行方式。

    12.2.4 数据驱动测试 (Data-Driven Testing)

    数据驱动测试 (Data-Driven Testing) 是一种测试技术,它 将测试数据与测试逻辑分离。测试用例的逻辑保持不变,但 使用不同的输入数据进行多次测试,以验证代码在不同输入情况下的行为。Qt Test 框架提供了对数据驱动测试的良好支持,通过 QTest::addColumnQTest::addRow 等宏,可以方便地实现数据驱动测试。

    数据驱动测试的优势:

    提高测试覆盖率 (test coverage)

    ⚝ 使用不同的输入数据进行测试,可以 更全面地覆盖代码的各种输入情况,包括正常情况、边界情况、异常情况等,从而提高测试覆盖率,更有效地发现潜在的 Bug。

    减少重复代码 (code duplication)

    ⚝ 对于需要使用多组数据进行测试的场景,数据驱动测试可以 避免编写大量的重复代码。只需要编写一次测试逻辑,然后通过提供不同的数据行,就可以进行多次测试。

    提高测试代码的可维护性 (maintainability)

    ⚝ 测试数据与测试逻辑分离,使得 测试代码更加清晰和易于维护。当需要修改测试数据时,只需要修改数据提供函数,而不需要修改测试用例函数的逻辑。

    易于扩展 (extensibility)

    ⚝ 当需要 增加新的测试数据 时,只需要在数据提供函数中 添加新的数据行 即可,无需修改测试用例函数的代码。

    Qt Test 数据驱动测试的实现:

    要实现数据驱动测试,需要 定义两个函数

    数据提供函数 (Data Provider Function)

    ⚝ 函数名通常是被测试函数的名字后面加上 _data 后缀,例如 testAddition_data 对应于 testAddition 测试用例函数。
    ⚝ 函数声明为 测试类中的 private slots 函数
    ⚝ 函数的作用是 定义数据列 (使用 QTest::addColumn)添加测试数据行 (使用 QTest::addRow)
    ⚝ Qt Test 框架会自动识别并调用数据提供函数,为后续的测试用例函数提供数据。

    测试用例函数 (Test Case Function)

    ⚝ 函数名通常是被测试函数的名字,例如 testAddition
    ⚝ 函数声明为 测试类中的 private slots 函数
    ⚝ 函数的作用是 编写测试逻辑,并 从数据提供函数提供的数据中获取当前数据行的值 (使用 QFETCH),然后使用这些数据进行测试和断言。
    ⚝ Qt Test 框架会 针对数据提供函数提供的每一行数据,都执行一次测试用例函数

    数据驱动测试的示例 (以加法测试为例):

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyTestDataDrivenTest : public QObject
    2 {
    3 Q_OBJECT
    4
    5 private slots:
    6 void testAddition_data(); // 数据提供函数
    7 void testAddition(); // 测试用例函数
    8
    9 private:
    10 Q_DECLARE_METADATA // 声明元数据
    11 };
    12
    13 void MyTestDataDrivenTest::testAddition_data()
    14 {
    15 QTest::addColumn<int>("a"); // 声明 int 类型的数据列 "a"
    16 QTest::addColumn<int>("b"); // 声明 int 类型的数据列 "b"
    17 QTest::addColumn<int>("sum"); // 声明 int 类型的数据列 "sum"
    18
    19 QTest::addRow("positive numbers") << 1 << 2 << 3; // 添加一行数据,行名为 "positive numbers",数据为 1, 2, 3
    20 QTest::addRow("negative numbers") << -1 << -2 << -3; // 添加一行数据,行名为 "negative numbers",数据为 -1, -2, -3
    21 QTest::addRow("zero and positive") << 0 << 5 << 5; // 添加一行数据,行名为 "zero and positive",数据为 0, 5, 5
    22 QTest::addRow("large numbers") << 1000 << 2000 << 3000; // 添加一行数据,行名为 "large numbers",数据为 1000, 2000, 3000
    23 }
    24
    25 void MyTestDataDrivenTest::testAddition()
    26 {
    27 QFETCH(int, a); // 从当前数据行获取列 "a" 的数据,存储到变量 a 中
    28 QFETCH(int, b); // 从当前数据行获取列 "b" 的数据,存储到变量 b 中
    29 QFETCH(int, sum); // 从当前数据行获取列 "sum" 的数据,存储到变量 sum 中
    30
    31 QCOMPARE(a + b, sum); // 使用获取的数据进行测试,断言 a + b 是否等于 sum
    32 }
    33
    34 // ... main 函数和 QTEST_MAIN 宏 (或 QApplication 和 QTest::qExec)

    在这个示例中,testAddition_data 函数定义了三个数据列 "a", "b", "sum",并添加了四行测试数据。testAddition 函数使用 QFETCH 宏从当前数据行获取数据,并进行加法测试。Qt Test 框架会 自动执行 testAddition 函数四次,每次使用 testAddition_data 函数提供的一行数据。

    数据驱动测试的适用场景:

    ⚝ 当需要 测试同一个函数或逻辑在不同输入下的行为 时。
    ⚝ 当需要 测试边界值、等价类、错误条件 等多种情况时。
    ⚝ 当 测试数据可以清晰地组织成表格形式 时。

    总结

    数据驱动测试是一种 高效、灵活 的测试技术,可以 提高测试覆盖率、减少重复代码、提高测试代码的可维护性和扩展性。Qt Test 框架通过 QTest::addColumnQTest::addRow 等宏,提供了 简单易用的数据驱动测试实现方式。在编写 Qt GUI 应用程序的单元测试时,可以根据需要灵活地运用数据驱动测试技术。

    12.3 GUI 程序调试技巧

    调试 (Debugging) 是软件开发过程中不可或缺的环节,尤其对于 GUI (图形用户界面) 应用程序,由于其 事件驱动 (event-driven)用户交互复杂 的特性,调试可能会更具挑战性。Qt Creator IDE 和 Qt 框架本身提供了一系列强大的 调试工具和技巧,可以帮助开发者有效地定位和解决 GUI 程序中的 Bug。

    12.3.1 断点调试:在 Qt Creator 中设置断点

    断点调试 (Breakpoint Debugging) 是最常用、最基础的调试技巧之一。它允许开发者在 代码的特定位置设置断点 (breakpoint),当程序执行到断点处时,程序会暂停执行,此时开发者可以 检查程序的状态,例如 变量的值调用堆栈 (call stack) 等,从而理解程序的执行流程,定位错误原因。

    在 Qt Creator 中设置断点的步骤:

    打开代码编辑器 (Code Editor)

    ⚝ 在 Qt Creator 中,打开需要调试的 源代码文件 (.cpp, .h, .qml 等)

    定位断点位置:

    ⚝ 在代码编辑器中,找到想要设置断点的代码行。断点通常设置在 可能出现错误的代码行关键逻辑代码行函数入口 等位置。

    设置断点:

    方法一:鼠标点击行号区域:在代码编辑器窗口的 左侧行号区域鼠标点击想要设置断点的行号。点击后,该行行号左侧会出现一个 红色的圆点 (断点图标),表示断点已设置成功。再次点击该行行号,可以 取消断点
    方法二:使用快捷键: 将光标移动到想要设置断点的代码行,按下快捷键 F9 (Windows/Linux) 或 Cmd+8 (macOS)。也可以使用快捷菜单 "Debug" (调试) -> "Toggle Breakpoint" (切换断点)

    启动调试器 (Debugger)

    ⚝ 点击 Qt Creator 的 调试按钮 (Debug) (通常是一个绿色的虫子图标),或使用快捷键 F5 (Windows/Linux) 或 Cmd+Y (macOS)
    ⚝ Qt Creator 会 启动调试器,并 运行程序。程序会 在设置的断点处暂停执行

    调试操作:

    当程序在断点处暂停时,可以使用 Qt Creator 提供的调试工具进行各种调试操作:

    查看变量值 (Variables):在 Qt Creator 的 "Debugger" (调试器) 视图中,可以查看 当前作用域 (scope) 内的变量值。可以展开 对象 (object) 查看其成员变量,甚至可以 修改变量的值,以便测试不同的运行路径。
    单步执行 (Stepping)
    ▮▮▮▮⚝ Step Over (单步跳过):快捷键 F10 (Windows/Linux) 或 Cmd+U (macOS)。执行当前行代码,然后 跳到下一行。如果当前行是函数调用,则 直接执行完整个函数,不会进入函数内部。
    ▮▮▮▮⚝ Step Into (单步进入):快捷键 F11 (Windows/Linux) 或 Cmd+I (macOS)。执行当前行代码。如果当前行是函数调用,则 进入函数内部,单步执行函数内的代码。
    ▮▮▮▮⚝ Step Out (单步跳出):快捷键 Shift+F11 (Windows/Linux) 或 Cmd+Shift+I (macOS)。如果当前在函数内部,则 执行完当前函数的剩余代码,然后 跳出函数,回到调用函数的位置
    ▮▮▮▮⚝ Run to Cursor (运行到光标处):快捷键 Ctrl+F10 (Windows/Linux) 或 Cmd+Shift+U (macOS)。程序 继续运行,直到光标所在的代码行处暂停

    查看调用堆栈 (Call Stack):在 Qt Creator 的 "Debugger" (调试器) 视图中,可以查看 当前程序的调用堆栈。调用堆栈显示了 函数调用的层次关系,可以帮助开发者 了解程序的执行路径,以及 当前代码是被哪个函数调用的

    设置条件断点 (Conditional Breakpoint)
    ▮▮▮▮⚝ 右键点击已设置的断点,选择 "Edit Breakpoint" (编辑断点)
    ▮▮▮▮⚝ 在断点编辑对话框中,可以 设置断点条件 (Condition)。断点条件是一个 布尔表达式。只有当程序执行到断点处,且断点条件为 真 (true) 时,程序才会暂停执行。
    ▮▮▮▮⚝ 条件断点可以帮助开发者 在满足特定条件时才暂停程序,例如当某个变量的值达到特定值时,或者当循环执行到特定次数时。

    删除断点 (Remove Breakpoint)
    ▮▮▮▮⚝ 方法一:鼠标点击红色圆点:在行号区域 点击红色的断点图标,可以 删除断点
    ▮▮▮▮⚝ 方法二:使用快捷键: 将光标移动到已设置断点的代码行,再次按下快捷键 F9 (Windows/Linux) 或 Cmd+8 (macOS)
    ▮▮▮▮⚝ 方法三:在 "Breakpoints" (断点) 视图中删除: 在 Qt Creator 的 "Debugger" (调试器) 视图中,切换到 "Breakpoints" (断点) 选项卡,可以 查看所有已设置的断点,并可以 选择性地删除断点,或者 禁用 (disable) 断点 (断点仍然存在,但程序执行到断点处不会暂停)。

    结束调试:

    ⚝ 点击 Qt Creator 的 停止调试按钮 (Stop Debugging) (通常是一个红色的正方形图标),或使用快捷键 Shift+F5 (Windows/Linux) 或 Cmd+Period (.) (macOS),可以 结束调试会话,程序会 终止运行

    断点调试的技巧:

    合理设置断点: 断点不宜设置过多,过多的断点会 降低调试效率。应 根据调试目标,有选择性地设置断点
    结合单步执行: 断点调试通常与 单步执行 (Step Over, Step Into, Step Out) 结合使用,可以 逐步跟踪程序的执行流程,深入了解代码的运行细节。
    善用条件断点: 对于循环或复杂逻辑,条件断点 可以帮助开发者 在特定情况下才暂停程序,避免在不关心的循环迭代或分支中浪费时间。
    结合日志输出: 断点调试可以与 日志输出 (qDebug, qWarning 等) 结合使用。在关键代码位置 添加日志输出,可以 在程序运行时记录一些关键信息,即使不设置断点,也可以通过查看日志了解程序的运行状态。

    总结

    断点调试是 Qt Creator IDE 提供的 最基本、最强大的调试工具 之一。通过 设置断点启动调试器单步执行查看变量值和调用堆栈 等操作,开发者可以 深入了解程序的执行流程快速定位和解决 Bug。熟练掌握断点调试技巧,对于提高 GUI 程序开发效率至关重要。

    12.3.2 日志输出:qDebug(), qWarning(), qCritical(), qFatal()

    日志输出 (Log Output) 是另一种常用的调试技巧。它允许开发者在代码中 插入日志输出语句记录程序运行时的信息,例如 变量的值函数调用的参数程序执行的路径错误信息 等。这些日志信息可以在程序运行时 输出到控制台 (console)日志文件 (log file) 中,供开发者 分析和诊断问题

    Qt 框架提供了四个主要的日志输出函数,位于 QDebug 类中:

    qDebug(const char *message, ...):

    功能输出调试信息 (debug message)。通常用于 记录程序运行时的状态信息,例如变量的值、程序执行到某个关键位置等。
    级别Debug 级别,是 最低的日志级别。在 Release (发布) 版本 中,qDebug() 输出默认会被 禁用,不会产生任何输出,以提高程序性能。在 Debug (调试) 版本 中,qDebug() 输出会 默认启用
    使用场景
    ▮▮▮▮⚝ 跟踪程序流程:在关键函数或代码块的入口和出口处添加 qDebug() 输出,记录程序执行路径。
    ▮▮▮▮⚝ 查看变量值:在代码中输出变量的值,以便了解程序运行时的状态。
    ▮▮▮▮⚝ 输出信息性消息:输出一些信息性消息,帮助开发者理解程序的行为。
    示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyClass::myFunction(int value)
    2 {
    3 qDebug() << "myFunction called with value:" << value; // 输出函数调用信息和参数值
    4
    5 if (value < 0) {
    6 qDebug() << "Value is negative, returning early."; // 输出条件判断信息
    7 return;
    8 }
    9
    10 // ... 其他代码
    11 }

    qWarning(const char *message, ...):

    功能输出警告信息 (warning message)。通常用于 记录程序运行时遇到的警告或异常情况,例如 不符合预期的输入资源可能不足潜在的错误风险 等。警告信息 不会导致程序崩溃或功能失效,但 可能预示着潜在的问题
    级别Warning 级别高于 Debug 级别。在 Release 版本 中,qWarning() 输出默认会被 启用,但可以通过配置 禁用。在 Debug 版本 中,qWarning() 输出会 默认启用
    使用场景
    ▮▮▮▮⚝ 记录不符合预期的输入:当函数接收到不符合预期的输入参数时,可以使用 qWarning() 输出警告信息。
    ▮▮▮▮⚝ 提示资源不足:当程序检测到资源可能不足 (如内存不足、文件句柄耗尽等) 时,可以使用 qWarning() 输出警告信息。
    ▮▮▮▮⚝ 提示潜在的错误风险:当程序执行到可能存在错误风险的代码路径时,可以使用 qWarning() 输出警告信息。
    示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyClass::loadFile(const QString &filePath)
    2 {
    3 QFile file(filePath);
    4 if (!file.exists()) {
    5 qWarning() << "File not found:" << filePath; // 文件不存在,输出警告信息
    6 return;
    7 }
    8
    9 if (!file.open(QIODevice::ReadOnly)) {
    10 qWarning() << "Failed to open file:" << filePath << "Error:" << file.errorString(); // 文件打开失败,输出警告信息和错误原因
    11 return;
    12 }
    13
    14 // ... 后续文件操作
    15 file.close();
    16 }

    qCritical(const char *message, ...):

    功能输出严重错误信息 (critical message)。通常用于 记录程序运行时遇到的严重错误,这些错误 可能导致程序功能部分失效不稳定。严重错误信息 通常需要开发者立即关注和处理
    级别Critical 级别高于 Warning 级别。在 Release 版本Debug 版本 中,qCritical() 输出都会 默认启用
    使用场景
    ▮▮▮▮⚝ 关键资源获取失败:当程序无法获取关键资源 (如数据库连接失败、网络连接失败等) 时,可以使用 qCritical() 输出严重错误信息。
    ▮▮▮▮⚝ 数据损坏或丢失:当程序检测到数据损坏或丢失时,可以使用 qCritical() 输出严重错误信息。
    ▮▮▮▮⚝ 程序逻辑严重错误:当程序执行到不应该到达的代码路径,或者出现逻辑上的严重错误时,可以使用 qCritical() 输出严重错误信息。
    示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 bool MyClass::connectDatabase()
    2 {
    3 QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL");
    4 db.setHostName("localhost");
    5 db.setDatabaseName("mydb");
    6 db.setUserName("user");
    7 db.setPassword("password");
    8
    9 if (!db.open()) {
    10 qCritical() << "Failed to connect to database! Error:" << db.lastError().text(); // 数据库连接失败,输出严重错误信息和错误原因
    11 return false;
    12 }
    13
    14 qDebug() << "Database connection successful.";
    15 return true;
    16 }

    qFatal(const char *message, ...):

    功能输出致命错误信息 (fatal message),并 立即终止程序运行。通常用于 记录程序运行时遇到的无法恢复的致命错误,这些错误 会导致程序无法继续正常运行,必须 立即终止
    级别Fatal 级别,是 最高的日志级别。在 Release 版本Debug 版本 中,qFatal() 输出都会 默认启用
    使用场景
    ▮▮▮▮⚝ 内存分配失败:当程序尝试分配内存但失败时 (如 mallocnew 返回空指针),可以使用 qFatal() 输出致命错误信息并终止程序。
    ▮▮▮▮⚝ 严重系统错误:当程序遇到无法处理的系统级错误 (如文件系统错误、硬件错误等) 时,可以使用 qFatal() 输出致命错误信息并终止程序。
    ▮▮▮▮⚝ 不可恢复的逻辑错误:当程序执行到不可恢复的逻辑错误时,可以使用 qFatal() 输出致命错误信息并终止程序。
    示例

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyClass::allocateMemory(size_t size)
    2 {
    3 void *ptr = malloc(size);
    4 if (ptr == nullptr) {
    5 qFatal() << "Memory allocation failed! Size:" << size; // 内存分配失败,输出致命错误信息并终止程序
    6 // 程序会在这里立即终止,不会执行后续代码
    7 }
    8
    9 qDebug() << "Memory allocated successfully.";
    10 free(ptr);
    11 }

    日志输出的配置:

    Qt 提供了 灵活的日志输出配置机制,可以 控制不同级别日志的输出行为,例如 是否输出输出到哪里 (控制台、文件等)输出格式 等。日志输出配置可以通过 环境变量配置文件代码编程方式 进行设置。

    日志输出的优点:

    非侵入式调试 (non-intrusive debugging):日志输出语句 不会暂停程序运行,可以在 程序运行时实时记录信息,不会像断点调试那样影响程序的执行流程。
    远程调试 (remote debugging):对于 远程部署的程序无法使用调试器的环境,日志输出是 主要的调试手段。可以通过查看远程程序的日志文件来分析问题。
    性能分析 (performance analysis):通过在关键代码段 添加时间戳日志,可以 分析程序的性能瓶颈,例如函数执行时间、代码段耗时等。

    日志输出的缺点:

    信息过载 (information overload):如果日志输出过多,可能会导致 日志信息过载,难以从中找到关键信息。需要 合理控制日志输出量,只输出必要的调试信息。
    性能影响 (performance impact):虽然 qDebug() 在 Release 版本中会被禁用,但在 Debug 版本中,过多的日志输出仍然 可能对程序性能产生一定影响。需要 权衡日志输出的详细程度和性能影响

    日志输出的最佳实践:

    选择合适的日志级别: 根据信息的重要程度和紧急程度,选择合适的日志级别 (qDebug, qWarning, qCritical, qFatal)。
    提供清晰的日志信息: 日志信息应该 简洁明了,能够 准确描述程序的状态和错误原因。包含 时间戳线程 ID文件名行号 等信息可以提高日志的可读性和分析效率。
    合理控制日志输出量: 只输出 必要的调试信息,避免日志信息过载。可以 根据日志级别和配置动态控制日志输出
    使用日志配置文件或环境变量: 将日志输出配置 外部化,方便在不同环境 (开发、测试、生产) 中 灵活调整日志输出行为,而无需修改代码。

    总结

    日志输出是 Qt GUI 程序调试中 非常重要的辅助手段。通过使用 qDebug, qWarning, qCritical, qFatal 等日志输出函数,开发者可以在程序运行时 记录各种信息,用于 跟踪程序流程查看变量值诊断错误分析性能。合理使用日志输出,可以 提高调试效率,并为 远程调试和性能分析 提供有力的支持。

    12.3.3 界面元素检查器 (Inspect):Qt Inspect 工具

    界面元素检查器 (Inspect) 是一种专门用于 GUI 应用程序调试的工具。它可以 实时查看和分析 GUI 程序的界面结构控件属性布局信息事件处理 等,帮助开发者 理解 GUI 程序的运行时状态定位界面显示和交互问题Qt Inspect 工具 (Qt Inspect Tool) 是 Qt 官方提供的界面元素检查器,可以方便地检查和调试 Qt GUI 应用程序。

    Qt Inspect 工具的主要功能:

    实时界面结构查看:

    控件树 (Widget Tree) 视图: Qt Inspect 可以 实时显示 GUI 程序的控件树结构,以 树状结构 展示 窗口 (window)控件 (widget)布局 (layout) 之间的 父子关系
    控件层叠顺序 (Stacking Order) 查看: 可以查看 控件的层叠顺序,了解控件在 Z 轴方向上的排列关系。
    控件类型和类名显示: 显示 控件的类型 (如 QPushButton, QLabel, QLineEdit 等)类名,方便开发者识别控件。

    控件属性查看与编辑:

    属性列表 (Property List) 视图: Qt Inspect 可以 列出选中控件的所有属性 (properties),包括 属性名属性值属性类型 等。
    实时属性值更新: 当控件的属性值在程序运行时发生变化时,Qt Inspect 会 实时更新属性列表中的值
    属性值编辑 (Property Editing): 允许开发者 在 Qt Inspect 中直接编辑控件的属性值,并 实时查看界面变化。这对于 快速测试界面效果调试界面布局问题 非常有用。

    布局信息查看:

    布局信息 (Layout Information) 显示: Qt Inspect 可以 显示选中控件的布局信息,包括 控件的位置 (x, y)大小 (width, height)边距 (margin)对齐方式 (alignment) 等。
    布局边界 (Layout Bounds) 可视化: 可以 在界面上高亮显示控件的布局边界,帮助开发者 理解布局管理器的布局行为调试布局问题

    事件处理分析:

    事件过滤器 (Event Filter) 功能: Qt Inspect 可以 作为事件过滤器 插入到 GUI 应用程序中,拦截和记录程序中发生的事件 (events),例如 鼠标事件 (mouse events)键盘事件 (key events)窗口事件 (window events) 等。
    事件列表 (Event List) 视图: Qt Inspect 会 显示拦截到的事件列表,包括 事件类型事件目标控件事件参数 等。
    事件传播路径 (Event Propagation Path) 查看: 可以 查看事件的传播路径,了解事件是如何在控件树中传递和处理的。

    信号与槽 (Signals and Slots) 分析:

    信号与槽连接 (Signal-Slot Connection) 查看: Qt Inspect 可以 显示控件的信号与槽连接信息,包括 信号名槽函数名连接方式 等。
    信号发射 (Signal Emission) 跟踪: 可以 跟踪信号的发射过程,查看 哪个控件发射了哪个信号连接到了哪些槽函数

    使用 Qt Inspect 工具的步骤:

    编译 Qt Inspect 工具:

    ⚝ Qt Inspect 工具的源代码通常 包含在 Qt SDK (软件开发工具包) 的 qttools 模块中
    ⚝ 需要 使用 Qt 编译工具 (如 qmake 或 CMake) 编译 Qt Inspect 工具,生成可执行文件。

    运行 Qt Inspect 工具:

    ⚝ 编译成功后,运行 Qt Inspect 工具的可执行文件

    连接到目标 GUI 应用程序:

    ⚝ 在 Qt Inspect 工具的主界面中,选择 "File" (文件) -> "Connect to Application" (连接到应用程序)
    ⚝ 在弹出的对话框中,输入目标 GUI 应用程序的进程 ID (Process ID, PID)
    ▮▮▮▮⚝ 获取进程 ID 的方法: 可以使用 操作系统提供的任务管理器 (Task Manager, Windows)活动监视器 (Activity Monitor, macOS)ps 命令 (Linux/macOS) 等工具查看正在运行的 GUI 应用程序的进程 ID。
    ⚝ 点击 "Connect" (连接) 按钮,Qt Inspect 工具会 连接到目标 GUI 应用程序

    使用 Qt Inspect 功能:

    ⚝ 连接成功后,Qt Inspect 工具会 实时显示目标 GUI 应用程序的界面信息
    ⚝ 可以使用 Qt Inspect 的各种功能,例如 控件树视图属性列表视图布局信息显示事件过滤器 等,检查和调试 GUI 界面

    Qt Inspect 工具的适用场景:

    界面布局调试: 当界面布局出现问题 (如控件位置错误、大小不合适、重叠遮挡等) 时,可以使用 Qt Inspect 工具 查看布局信息高亮显示布局边界编辑控件属性,快速定位和解决布局问题。
    控件属性调试: 当控件的显示效果不符合预期 (如文本颜色错误、字体大小不合适、背景颜色错误等) 时,可以使用 Qt Inspect 工具 查看控件属性编辑属性值,实时调整控件样式。
    事件处理调试: 当 GUI 程序的事件处理逻辑出现问题 (如控件无法响应鼠标点击、键盘输入无效等) 时,可以使用 Qt Inspect 工具 拦截和记录事件查看事件传播路径,分析事件处理流程。
    信号与槽调试: 当信号与槽连接出现问题 (如信号未发射、槽函数未执行等) 时,可以使用 Qt Inspect 工具 查看信号与槽连接信息跟踪信号发射,分析信号与槽机制的运行情况。

    Qt Inspect 工具的局限性:

    依赖于目标程序进程: Qt Inspect 工具需要 连接到正在运行的 GUI 应用程序进程 才能工作。如果程序崩溃或无法正常运行,Qt Inspect 工具也无法使用。
    部分功能可能受限: 对于一些 复杂的 GUI 程序使用了自定义控件 的程序,Qt Inspect 工具的 部分功能可能受限,例如无法完全解析自定义控件的属性和事件。

    总结

    Qt Inspect 工具是 Qt GUI 程序调试的 利器。它提供了 实时界面结构查看控件属性查看与编辑布局信息查看事件处理分析信号与槽分析 等功能,可以帮助开发者 深入了解 GUI 程序的运行时状态快速定位和解决界面显示和交互问题。在 Qt GUI 开发过程中,熟练使用 Qt Inspect 工具可以 显著提高调试效率提升开发体验

    12.3.4 内存泄漏检测工具:Valgrind, AddressSanitizer

    内存泄漏 (Memory Leak) 是指 程序在动态分配内存后,未能及时释放已经不再使用的内存,导致 系统可用内存逐渐减少,最终 可能导致程序性能下降、崩溃 甚至 系统崩溃。对于 C++ GUI 应用程序,由于 动态内存分配和释放频繁内存泄漏问题尤为需要关注内存泄漏检测工具 (Memory Leak Detection Tools) 可以帮助开发者 自动检测程序中的内存泄漏问题,并 提供详细的泄漏信息,方便开发者定位和修复内存泄漏。

    常用的内存泄漏检测工具:

    Valgrind (瓦尔格朗德):

    功能强大的内存分析工具: Valgrind 是一套 开源的、跨平台的内存分析工具集,其中最常用的工具是 Memcheck (内存检查)。Memcheck 可以 检测多种内存错误,包括 内存泄漏非法内存访问 (如读写已释放的内存、访问未初始化的内存)内存越界 等。
    工作原理: Valgrind 使用 虚拟 CPU 技术,在 虚拟机上运行程序,并 监控程序的内存操作。当程序发生内存错误时,Valgrind 会 拦截并记录错误信息
    优点:
    ▮▮▮▮⚝ 功能强大,检测准确: Valgrind Memcheck 可以 检测多种内存错误,包括 各种类型的内存泄漏检测精度高
    ▮▮▮▮⚝ 跨平台支持: Valgrind 支持 Linux, macOS, Android 等多个操作系统平台。
    ▮▮▮▮⚝ 详细的错误报告: Valgrind 会 提供详细的错误报告,包括 错误类型错误发生位置内存分配和释放的调用堆栈 等,方便开发者定位错误代码。
    ▮▮▮▮⚝ 开源免费: Valgrind 是 开源免费 的,可以 免费使用和分发
    缺点:
    ▮▮▮▮⚝ 性能开销大: Valgrind 在 虚拟机上运行程序,并进行 细粒度的内存监控,因此 性能开销较大,程序运行速度会 明显变慢 (通常会慢 10-50 倍)。不适合在性能敏感的场景中使用,通常用于 调试和测试阶段
    ▮▮▮▮⚝ 使用相对复杂: Valgrind 的 命令行参数较多,使用起来 相对复杂,需要一定的学习成本。

    使用 Valgrind 检测内存泄漏的步骤:

    1. 安装 Valgrind: 在操作系统上 安装 Valgrind 工具
    2. 编译程序: 使用 Debug (调试) 模式 编译需要检测的 Qt GUI 应用程序。
    3. 运行 Valgrind: 在命令行终端中,使用 valgrind 命令 运行程序,并 指定 Memcheck 工具内存泄漏检测选项。例如:
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 valgrind --tool=memcheck --leak-check=full ./my_gui_app

    ▮▮▮▮⚝ --tool=memcheck: 指定使用 Valgrind 的 Memcheck 工具。
    ▮▮▮▮⚝ --leak-check=full: 启用 完整的内存泄漏检测
    ▮▮▮▮⚝ ./my_gui_app: 需要检测的 Qt GUI 应用程序的可执行文件路径。
    4. 分析 Valgrind 输出: 运行程序后,Valgrind 会 输出内存分析报告到命令行终端。报告中会 列出检测到的内存错误,包括 内存泄漏信息 (如 "definitely lost", "indirectly lost", "possibly lost" 等)错误发生位置内存分配和释放的调用堆栈 等。
    5. 定位和修复内存泄漏: 根据 Valgrind 的错误报告,定位程序中发生内存泄漏的代码检查内存分配和释放逻辑确保动态分配的内存最终都被正确释放

    AddressSanitizer (地址清理器):

    轻量级的内存错误检测工具: AddressSanitizer (简称 ASan) 是 Google 开发的、轻量级的内存错误检测工具。它可以 检测多种内存错误,包括 内存泄漏非法内存访问堆栈溢出use-after-free 等。
    工作原理: AddressSanitizer 使用 编译器插桩技术 (compiler instrumentation),在 编译时在代码中插入内存检查代码。程序运行时,这些检查代码会 监控程序的内存操作,当程序发生内存错误时,AddressSanitizer 会 立即报告错误
    优点:
    ▮▮▮▮⚝ 性能开销小: AddressSanitizer 的 性能开销相对较小,程序运行速度 只会有轻微的减慢 (通常慢 2 倍左右),比 Valgrind 快得多。更适合在日常开发和测试中使用
    ▮▮▮▮⚝ 检测速度快: AddressSanitizer 可以 立即报告内存错误错误定位速度快
    ▮▮▮▮⚝ 易于使用: AddressSanitizer 的 使用非常简单,只需要在 编译和链接时添加相应的编译器和链接器选项 即可。
    ▮▮▮▮⚝ 开源免费: AddressSanitizer 是 开源免费 的,可以 免费使用和分发
    缺点:
    ▮▮▮▮⚝ 检测范围相对有限: AddressSanitizer 的 检测范围相对 Valgrind 较小,主要关注 堆内存错误,对于 栈内存错误部分类型的内存泄漏 检测能力相对较弱。
    ▮▮▮▮⚝ 平台支持相对有限: AddressSanitizer 的 平台支持相对 Valgrind 较窄,主要支持 Linux, macOS, Android 等平台,对 Windows 平台的支持相对较弱

    使用 AddressSanitizer 检测内存泄漏的步骤:

    1. 安装 AddressSanitizer: AddressSanitizer 通常 集成在现代编译器 (如 GCC, Clang) 中。确保使用的编译器 支持 AddressSanitizer
    2. 编译程序: 使用 支持 AddressSanitizer 的编译器,并 添加编译器和链接器选项 启用 AddressSanitizer。例如,在使用 GCC 或 Clang 编译 Qt 项目时,可以在 .pro 文件中添加以下编译选项:
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QMAKE_CXXFLAGS += -fsanitize=address
    2 QMAKE_LDFLAGS += -fsanitize=address

    或者在 CMakeLists.txt 中添加:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")
    2 set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
    1. 运行程序: 直接运行编译后的程序
    2. 分析 AddressSanitizer 输出: 当程序运行时,如果 检测到内存错误,AddressSanitizer 会 立即输出错误报告到命令行终端。报告中会 包含错误类型错误发生位置内存分配和释放的调用堆栈 等。
    3. 定位和修复内存泄漏: 根据 AddressSanitizer 的错误报告,定位程序中发生内存泄漏的代码检查内存分配和释放逻辑确保动态分配的内存最终都被正确释放

    选择内存泄漏检测工具:

    Valgrind: 如果 需要高精度的内存错误检测可以容忍较大的性能开销,或者需要 检测多种类型的内存错误,可以选择 Valgrind
    AddressSanitizer: 如果 追求性能开销小检测速度快更适合在日常开发和测试中使用,或者主要关注 堆内存错误,可以选择 AddressSanitizer

    对于 Qt GUI 应用程序的内存泄漏检测,建议在开发和测试阶段同时使用 Valgrind 和 AddressSanitizerAddressSanitizer 用于日常快速检测Valgrind 用于更深入、更全面的内存分析

    总结

    内存泄漏是 GUI 程序质量的隐患。ValgrindAddressSanitizer 是强大的内存泄漏检测工具,可以帮助开发者 自动检测程序中的内存泄漏问题,并 提供详细的泄漏信息。熟练使用这些工具,可以 有效地预防和解决内存泄漏问题提高 Qt GUI 应用程序的质量和稳定性

    13. 部署与发布:应用程序打包

    章节概要

    本章将深入探讨 GUI 应用程序的部署与发布流程,重点讲解如何将开发完成的 Qt GUI 应用程序打包,使其能够在目标用户的操作系统平台上独立运行。无论是 Windows、macOS 还是 Linux,应用程序的部署都是软件生命周期中至关重要的一环。本章将涵盖不同平台上的部署策略、依赖库处理、打包工具的使用,以及发布过程中的常见问题和最佳实践,旨在帮助读者顺利地将自己的 Qt GUI 应用交付给最终用户。

    13.1 应用程序部署概述

    部署 (Deployment) 是将开发完成的应用程序从开发环境转移到目标运行环境的过程。对于 GUI 应用程序而言,部署不仅包括程序本身,还需要处理其依赖的各种资源和库,确保用户在没有安装开发环境的情况下也能正常运行程序。本节将概述应用程序部署的基本概念、目标平台以及部署类型。

    13.1.1 部署的目标平台:Windows, macOS, Linux

    部署的首要步骤是明确应用程序的目标平台 (Target Platform)。常见的桌面操作系统平台包括:

    Windows: 微软 (Microsoft) 公司的 Windows 系列操作系统,是桌面应用最广泛的平台。Windows 平台具有庞大的用户基数,因此成为许多桌面应用程序的首选目标平台。

    macOS: 苹果 (Apple) 公司的 macOS 操作系统,主要用于苹果的 Mac 系列电脑。macOS 以其优秀的图形界面和用户体验而闻名,是专业软件和创意应用的重要平台。

    Linux: 基于 Linux 内核的开源操作系统,拥有众多发行版 (Distributions),如 Ubuntu, Fedora, Debian, CentOS 等。Linux 平台在服务器、嵌入式系统和开发者社区中广泛使用,对于跨平台应用而言是不可或缺的目标。

    针对不同的目标平台,应用程序的部署策略和打包方式会有所不同。例如,Windows 平台通常需要处理动态链接库 (DLLs) 依赖,macOS 平台需要创建应用程序包 (Application Bundle),而 Linux 平台则需要考虑不同的发行版和依赖库管理方式。跨平台部署是 Qt 框架的一大优势,但同时也需要开发者了解各平台之间的差异,并采取相应的部署策略。

    13.1.2 部署的类型:静态部署 (Static Deployment), 动态部署 (Dynamic Deployment)

    应用程序部署主要分为两种类型:静态部署 (Static Deployment) 和动态部署 (Dynamic Deployment)。这两种部署类型的主要区别在于如何处理应用程序的依赖库,尤其是 Qt 库。

    静态部署 (Static Deployment)

    静态部署是指将应用程序所有的依赖库,包括 Qt 库以及其他第三方库,全部编译并链接到可执行文件 (Executable File) 中。这意味着最终生成的可执行文件是一个独立的、自包含的程序,运行时不需要额外的外部库文件。

    优点

    ▮▮▮▮⚝ 易于分发:由于应用程序是自包含的,用户下载后可以直接运行,无需安装额外的运行时库。
    ▮▮▮▮⚝ 版本冲突少:静态链接避免了因用户系统上安装的库版本与应用程序所需版本不一致而导致的问题。

    缺点

    ▮▮▮▮⚝ 文件体积大:静态链接会将所有依赖库的代码都复制到可执行文件中,导致文件体积显著增大。
    ▮▮▮▮⚝ 更新维护难:如果依赖库(例如 Qt 库)有安全更新或 bug 修复,需要重新编译和发布整个应用程序。
    ▮▮▮▮⚝ 编译复杂:静态编译 Qt 需要特殊的配置和编译过程,相对较为复杂。

    动态部署 (Dynamic Deployment)

    动态部署是指应用程序在运行时依赖外部的动态链接库 (Dynamic Link Library, DLL)。这意味着应用程序的可执行文件本身体积较小,但需要同时发布应用程序所依赖的 Qt 库和其他第三方库的动态链接库文件。

    优点

    ▮▮▮▮⚝ 文件体积小:动态链接的可执行文件体积较小,发布包的整体体积也相对较小。
    ▮▮▮▮⚝ 库共享:如果多个应用程序依赖相同的动态库(例如 Qt 库),可以共享使用,节省磁盘空间和内存。
    ▮▮▮▮⚝ 更新维护方便:当依赖库需要更新时,只需要替换动态库文件即可,无需重新编译和发布整个应用程序。

    缺点

    ▮▮▮▮⚝ 依赖库管理:需要确保目标系统上存在应用程序所需的动态库,或者将这些动态库与应用程序一起发布。
    ▮▮▮▮⚝ 版本冲突风险:如果用户系统上已存在相同名称但版本不兼容的动态库,可能会导致运行时问题。

    对于 Qt GUI 应用程序而言,动态部署是更为常见和推荐的方式。Qt 库本身非常庞大,静态链接会导致最终程序体积过大,不利于分发和更新。因此,本章后续章节将主要围绕动态部署展开讨论。

    13.1.3 依赖库 (Dependencies) 处理:Qt 库、第三方库

    无论是静态部署还是动态部署,处理应用程序的依赖库 (Dependencies) 都是部署过程中不可或缺的环节。对于 Qt GUI 应用程序,主要的依赖库包括:

    Qt 库 (Qt Libraries)

    Qt 框架本身是由一系列模块化的库组成的,例如 QtCore(核心模块), QtGui(图形界面基础模块), QtWidgets(经典控件模块), QtNetwork(网络编程模块)等等。开发者在编写 Qt GUI 应用程序时,会根据需求使用不同的 Qt 模块。因此,部署应用程序时,必须包含程序所依赖的 Qt 模块的动态库文件。

    例如,如果应用程序使用了 QtWidgets 模块和 QtNetwork 模块,那么在动态部署时,就需要将 QtWidgetsQtNetwork 模块对应的 DLLs (在 Windows 平台) 或 Shared Libraries (在 Linux 平台) 一起发布。

    第三方库 (Third-party Libraries)

    除了 Qt 库之外,应用程序可能还会依赖其他第三方库。例如,如果应用程序使用了某个图像处理库、数据库客户端库或者加密库,那么在部署时,也需要将这些第三方库的动态链接库文件一并处理。

    处理第三方库的方式与 Qt 库类似,需要根据库的类型和部署方式,将其动态链接库文件放置在应用程序可以找到的位置。

    平台特定库 (Platform-specific Libraries)

    某些应用程序可能还会依赖于特定操作系统平台上的系统库 (System Libraries)。这些库通常是操作系统自带的,例如 Windows 上的 msvcrt.dll 或 Linux 上的 libc.so。在大多数情况下,这些系统库已经存在于目标系统上,无需额外处理。但在某些特殊情况下,例如需要特定版本的系统库,或者目标系统环境较为特殊时,可能也需要考虑平台特定库的依赖问题。

    在动态部署中,正确处理依赖库是确保应用程序能够成功运行的关键。后续章节将针对 Windows, macOS, Linux 三个平台,分别详细介绍如何处理 Qt 库和其他依赖库的部署问题。

    13.2 Windows 平台部署

    Windows 平台是桌面应用程序的重要阵地。本节将详细介绍 Windows 平台下 Qt GUI 应用程序的动态部署方法,包括 Qt DLLs 依赖处理、windeployqt 工具的使用以及安装包的创建。

    13.2.1 动态部署:Qt DLLs 依赖

    在 Windows 平台上进行 Qt GUI 应用程序的动态部署,核心在于处理 Qt DLLs 依赖。Qt 的每个模块都对应着一个或多个 DLL 文件。例如,QtCore 模块对应 Qt5Core.dll (Qt 5 版本) 或 Qt6Core.dll (Qt 6 版本),QtWidgets 模块对应 Qt5Widgets.dllQt6Widgets.dll 等等。

    当应用程序运行时,操作系统需要能够找到这些 DLL 文件。通常情况下,Windows 操作系统会按照一定的搜索路径 (Search Path) 查找 DLL 文件。为了确保应用程序能够找到所需的 Qt DLLs,常见的做法是将这些 DLL 文件复制到与应用程序可执行文件相同的目录下

    具体而言,对于一个 Qt GUI 应用程序 MyApp.exe,如果它依赖于 QtCore, QtGui, QtWidgets 这三个模块,那么在发布时,至少需要将以下 DLL 文件复制到 MyApp.exe 所在的目录下:

    Qt5Core.dllQt6Core.dll (取决于 Qt 版本)
    Qt5Gui.dllQt6Gui.dll
    Qt5Widgets.dllQt6Widgets.dll
    ⚝ 以及其他程序可能依赖的 Qt 模块的 DLLs,例如 Qt5Network.dll, Qt5Sql.dll 等。

    除了 Qt 模块的 DLLs,应用程序还可能依赖于平台插件 (Platform Plugins)。例如,Qt 使用平台插件来处理不同操作系统平台的窗口系统和图形界面。Windows 平台的平台插件通常位于 Qt 安装目录下的 plugins\platforms 文件夹中,例如 qwindows.dll平台插件 DLLs 也需要与应用程序一起发布,通常需要将整个 platforms 文件夹复制到应用程序可执行文件所在的目录下。

    手动复制 DLLs 的缺点

    容易遗漏:手动查找和复制依赖的 DLLs 容易出错,可能会遗漏某些必要的 DLL 文件。
    版本不匹配:如果开发环境和目标系统上的 Qt 版本不一致,手动复制 DLLs 可能会导致版本不匹配的问题。
    繁琐:对于复杂的应用程序,依赖的 Qt 模块和平台插件较多,手动复制 DLLs 过程繁琐且容易出错。

    为了解决手动复制 DLLs 的问题,Qt 官方提供了 windeployqt 工具,可以自动分析应用程序的依赖,并将所需的 Qt DLLs 和平台插件复制到指定的目录。

    13.2.2 使用 windeployqt 工具自动部署 Qt 依赖

    windeployqt 是 Qt 官方提供的命令行工具,用于自动化部署 Windows 平台上的 Qt 应用程序依赖。它可以扫描指定目录下的可执行文件,分析其依赖的 Qt 模块和平台插件,并将必要的 DLLs 和插件复制到与可执行文件相同的目录下,或者指定的输出目录。

    windeployqt 工具的使用方法

    1. 打开 Qt 命令行工具:在 Windows 开始菜单中找到 Qt 安装目录下的 "Qt <版本> <架构> Command Prompt" 并打开。例如,"Qt 6.5.0 MSVC2019 64-bit Command Prompt"。

    2. 导航到应用程序可执行文件所在的目录:使用 cd 命令切换到应用程序可执行文件 (.exe 文件) 所在的目录。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 cd path\to\your\application\release
    1. 运行 windeployqt 命令:在当前目录下直接运行 windeployqt 命令。windeployqt 会自动扫描当前目录下的可执行文件,并复制所需的 Qt DLLs 和平台插件到当前目录。
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 windeployqt .

    或者,如果你的可执行文件不在当前目录,可以指定可执行文件的路径作为 windeployqt 的参数:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 windeployqt MyApp.exe
    1. 检查部署结果windeployqt 运行完成后,检查应用程序可执行文件所在的目录。应该可以看到复制过来的 Qt DLLs 文件(例如 Qt6Core.dll, Qt6Gui.dll, Qt6Widgets.dll 等)以及 platforms 文件夹(包含 qwindows.dll)。

    windeployqt 常用选项

    -binaries <目录>:指定额外的二进制文件目录,windeployqt 会扫描这些目录下的可执行文件和 DLLs,并将其依赖也复制过来。例如,如果应用程序依赖于第三方 DLLs,可以使用此选项指定第三方 DLLs 所在的目录。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 windeployqt --binaries path\to\thirdparty\dlls .

    -libdir <目录>:指定 Qt 库的输出目录。默认情况下,windeployqt 会将 Qt DLLs 复制到与可执行文件相同的目录。使用此选项可以指定一个不同的输出目录。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 windeployqt --libdir qt_libs .

    -plugindir <目录>:指定 Qt 插件的输出目录。默认情况下,插件会被复制到可执行文件目录下的 plugins 子目录。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 windeployqt --plugindir qt_plugins .

    -no-angle:排除 ANGLE (Almost Native Graphics Layer Engine) 相关的 DLLs。ANGLE 是一个将 OpenGL ES 转换为 Direct3D 的库,Qt 在某些情况下会使用 ANGLE 作为 OpenGL 的后端。如果应用程序不需要 ANGLE,可以使用此选项排除相关 DLLs,减小部署包体积。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 windeployqt --no-angle .

    -verbose <级别>:设置详细输出级别。级别可以是 0 (默认,只输出错误信息), 1 (输出警告和错误信息), 2 (输出所有信息,包括调试信息)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 windeployqt --verbose 2 .

    使用 windeployqt 工具可以大大简化 Windows 平台 Qt 应用程序的依赖部署过程,减少手动操作的错误,并确保应用程序能够正确找到所需的 Qt 库和平台插件。

    13.2.3 创建安装包:Inno Setup, NSIS

    完成 Qt 依赖部署后,为了方便用户安装和卸载应用程序,通常需要创建一个安装包 (Installation Package)。在 Windows 平台上,常用的安装包制作工具包括 Inno Setup 和 NSIS (Nullsoft Scriptable Install System)。

    Inno Setup

    Inno Setup 是一个免费的 Windows 安装包制作工具,功能强大且易于使用。它使用脚本 (Script) 语言来定义安装过程和安装界面,可以创建专业的 Windows 安装程序。

    Inno Setup 的主要特点

    ▮▮▮▮⚝ 脚本驱动:使用 Pascal 风格的脚本语言来定义安装过程,灵活且可定制性强。
    ▮▮▮▮⚝ 图形界面:提供友好的图形界面编辑器,方便创建和编辑安装脚本。
    ▮▮▮▮⚝ 丰富的功能:支持创建快捷方式、注册表项、文件关联、自定义安装界面、多语言支持、自动更新等功能。
    ▮▮▮▮⚝ 体积小巧:生成的安装程序体积小,运行效率高。
    ▮▮▮▮⚝ 免费开源:Inno Setup 本身是免费且开源的。

    使用 Inno Setup 创建安装包的基本步骤

    1. 下载和安装 Inno Setup:从 Inno Setup 官网 (https://jrsoftware.org/isinfo.php) 下载并安装 Inno Setup 工具。

    2. 启动 Inno Setup 编译器:运行 Inno Setup 编译器 (Inno Setup Compiler)。

    3. 创建新的安装脚本:选择 "File" -> "New" 创建一个新的安装脚本。Inno Setup 提供了向导 (Wizard) 来引导用户创建基本的安装脚本。

    4. 配置安装脚本:根据向导或手动编辑安装脚本,配置应用程序的信息、安装目录、要安装的文件、快捷方式、卸载程序等等。

    一个基本的 Inno Setup 脚本示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 [Setup]
    2 AppName=MyApp
    3 AppVersion=1.0.0
    4 DefaultDirName={pf}\MyApp
    5 DefaultGroupName=MyApp
    6 OutputDir=.\Output
    7 OutputBaseFilename=MyAppSetup
    8 Compression=lzma
    9 SolidCompression=yes
    10
    11 [Files]
    12 Source: "MyApp.exe"; DestDir: "{app}"
    13 Source: "Qt6Core.dll"; DestDir: "{app}"
    14 Source: "Qt6Gui.dll"; DestDir: "{app}"
    15 Source: "Qt6Widgets.dll"; DestDir: "{app}"
    16 Source: "platforms\*"; DestDir: "{app}\platforms"; Flags: recursesubdirs createallsubdirs
    17 ; 添加更多需要安装的文件和文件夹
    18
    19 [Icons]
    20 Name: "{group}\MyApp"; Filename: "{app}\MyApp.exe"
    21
    22 [UninstallRun]
    23 Filename: "{app}\MyApp.exe"; Parameters: "--uninstall" ; 如果程序支持卸载功能
    1. 编译安装脚本:点击 "Run" -> "Compile" 编译安装脚本。Inno Setup 会根据脚本生成一个可执行的安装程序 (.exe 文件),例如 MyAppSetup.exe

    2. 测试安装程序:运行生成的安装程序,测试安装过程是否正常,包括文件复制、快捷方式创建、卸载功能等等。

    NSIS (Nullsoft Scriptable Install System)

    NSIS 也是一个免费开源的 Windows 安装包制作工具,历史悠久,广泛使用。NSIS 同样使用脚本语言来定义安装过程,但其脚本语言与 Inno Setup 不同。

    NSIS 的主要特点

    ▮▮▮▮⚝ 脚本驱动:使用基于宏的脚本语言来定义安装过程,功能强大且灵活。
    ▮▮▮▮⚝ 体积小巧:生成的安装程序体积非常小。
    ▮▮▮▮⚝ 插件丰富:NSIS 拥有大量的插件,可以扩展其功能,例如网络下载、数据库操作、界面定制等。
    ▮▮▮▮⚝ 开源免费:NSIS 本身是开源且免费的。

    使用 NSIS 创建安装包的基本步骤

    1. 下载和安装 NSIS:从 NSIS 官网 (https://nsis.sourceforge.io/Main_Page) 下载并安装 NSIS 工具。

    2. 创建新的 NSIS 脚本:使用文本编辑器创建一个新的 .nsi 文件,作为 NSIS 脚本文件。

    3. 编写 NSIS 脚本:根据 NSIS 脚本语法,编写安装脚本,配置应用程序的信息、安装目录、要安装的文件、快捷方式、卸载程序等等。

    一个基本的 NSIS 脚本示例:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 !define APPNAME "MyApp"
    2 !define APPVERSION "1.0.0"
    3 !define APPDIR "$PROGRAMFILES\${APPNAME}"
    4 !define OUTPUTFILE "MyAppSetup.exe"
    5
    6 OutFile "${OUTPUTFILE}"
    7 InstallDir "${APPDIR}"
    8 InstallDirRegKey HKCU "Software\${APPNAME}" InstallLocation
    9
    10 Section "MainSection" SEC01
    11 SetOutPath "$INSTDIR"
    12 File "MyApp.exe"
    13 File "Qt6Core.dll"
    14 File "Qt6Gui.dll"
    15 File "Qt6Widgets.dll"
    16 File /r "platforms\*" ; 递归复制 platforms 文件夹
    17 ; 添加更多需要安装的文件和文件夹
    18
    19 CreateDirectory "$INSTDIR\Uninstall"
    20 WriteUninstaller "$INSTDIR\Uninstall\Uninstall.exe"
    21 SectionEnd
    22
    23 Section "UninstallSection" SEC02
    24 Delete "$INSTDIR\MyApp.exe"
    25 Delete "$INSTDIR\Qt6Core.dll"
    26 Delete "$INSTDIR\Qt6Gui.dll"
    27 Delete "$INSTDIR\Qt6Widgets.dll"
    28 RMDir /r "$INSTDIR\platforms"
    29 RMDir "$INSTDIR\Uninstall"
    30 RMDir "$INSTDIR"
    31 DeleteRegKey HKCU "Software\${APPNAME}"
    32 SectionEnd
    33
    34 Section "ShortcutsSection" SEC03
    35 CreateDirectory "$SMPROGRAMS\${APPNAME}"
    36 CreateShortCut "$SMPROGRAMS\${APPNAME}\${APPNAME}.lnk" "$INSTDIR\MyApp.exe"
    37 SectionEnd
    1. 编译 NSIS 脚本:打开 NSIS 编译器 (MakeNSISW.exe),加载 .nsi 脚本文件,点击 "Compile NSIS Script" 编译脚本。NSIS 会生成一个可执行的安装程序 (.exe 文件),例如 MyAppSetup.exe

    2. 测试安装程序:运行生成的安装程序,测试安装过程是否正常,包括文件复制、快捷方式创建、卸载功能等等。

    Inno Setup 和 NSIS 都是优秀的 Windows 安装包制作工具,开发者可以根据自己的需求和偏好选择合适的工具。Inno Setup 界面更友好,脚本更易上手,适合快速创建基本的安装包;NSIS 脚本功能更强大,插件更丰富,适合创建更复杂的、高度定制化的安装包。

    13.3 macOS 平台部署

    macOS 平台以其独特的应用程序包 (Application Bundle) 结构和代码签名 (Code Signing) 与公证 (Notarization) 机制而闻名。本节将深入探讨 macOS 平台下 Qt GUI 应用程序的部署方法,包括应用程序包结构、Qt Frameworks 依赖处理、macdeployqt 工具的使用以及代码签名与公证流程。

    13.3.1 应用程序包 (Application Bundle) 结构

    在 macOS 平台上,应用程序通常以应用程序包 (Application Bundle) 的形式存在。应用程序包是一个特殊的文件夹,后缀名为 .app,例如 MyApp.app。在 Finder 中,应用程序包会被显示为一个单独的应用程序图标,用户可以直接双击运行。

    应用程序包的内部结构

    一个典型的 macOS 应用程序包包含以下主要目录和文件:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 MyApp.app/
    2 ├── Contents/
    3 │ ├── MacOS/
    4 │ │ └── MyApp (可执行文件)
    5 │ ├── Resources/
    6 │ │ ├── MyApp.icns (应用程序图标)
    7 │ │ └── ... (其他资源文件,如翻译文件、图片等)
    8 │ └── Info.plist (应用程序信息文件)
    9 └── ... (其他文件,如 Frameworks 目录,可选)

    MyApp.app/Contents/MacOS/MyApp: 这是应用程序的可执行文件。在应用程序包中,可执行文件必须放置在 Contents/MacOS 目录下,并且不带任何后缀名

    MyApp.app/Contents/Resources/MyApp.icns: 这是应用程序的图标文件,通常为 .icns 格式。图标文件用于在 Finder 和 Dock 中显示应用程序的图标。

    MyApp.app/Contents/Resources/: 这个目录用于存放应用程序的资源文件,例如图片、声音、翻译文件、配置文件等等。

    MyApp.app/Contents/Info.plist: 这是一个 XML 文件,用于描述应用程序的元数据,例如应用程序的名称、版本号、Bundle Identifier (包标识符)、图标文件、支持的文档类型等等。macOS 系统会读取 Info.plist 文件来获取应用程序的信息。

    MyApp.app/Contents/Frameworks/ (可选): 如果应用程序依赖于动态链接库,例如 Qt Frameworks 或其他第三方 Frameworks,通常需要将这些 Frameworks 放置在 Contents/Frameworks 目录下。

    创建应用程序包

    创建 macOS 应用程序包的本质就是创建一个特定结构的文件夹,并将可执行文件、资源文件、图标文件、Info.plist 文件以及依赖的 Frameworks 按照上述结构放置在相应的目录下,并将文件夹后缀名改为 .app

    可以使用命令行手动创建应用程序包,也可以使用构建工具(例如 CMake)或打包工具来自动化创建应用程序包的过程。

    13.3.2 动态部署:Qt Frameworks 依赖

    在 macOS 平台上进行 Qt GUI 应用程序的动态部署,需要处理 Qt Frameworks 依赖。与 Windows 平台上的 DLLs 类似,Qt 在 macOS 平台上以 Frameworks (框架) 的形式提供动态库。例如,QtCore 模块对应 QtCore.frameworkQtGui 模块对应 QtGui.frameworkQtWidgets 模块对应 QtWidgets.framework 等等。

    macOS Frameworks 的特点

    文件夹结构:macOS Frameworks 本质上也是文件夹,后缀名为 .framework,例如 QtCore.framework。Frameworks 文件夹内部包含动态库文件、头文件、资源文件等。

    版本控制:macOS Frameworks 支持版本控制,可以在同一个 Frameworks 文件夹下存放多个版本的动态库,并使用符号链接 (Symbolic Link) 来指向当前使用的版本。

    自包含:macOS Frameworks 通常是自包含的,一个 Frameworks 文件夹包含了该模块所需的所有资源,例如动态库、头文件、资源文件等。

    Qt Frameworks 部署

    对于 Qt GUI 应用程序,如果采用动态部署,需要将应用程序所依赖的 Qt Frameworks 复制到应用程序包的 Contents/Frameworks 目录下。例如,如果应用程序 MyApp.app 依赖于 QtCore, QtGui, QtWidgets 这三个模块,那么需要将 QtCore.framework, QtGui.framework, QtWidgets.framework 这三个 Frameworks 文件夹复制到 MyApp.app/Contents/Frameworks/ 目录下。

    Qt Frameworks 通常位于 Qt 安装目录下的 lib 文件夹中,例如 /Users/username/Qt/Qt6.5.0/macos/lib/.

    手动复制 Frameworks 的缺点

    与 Windows 平台手动复制 DLLs 类似,手动复制 Frameworks 也存在容易遗漏、版本不匹配、过程繁琐等缺点。为了解决这些问题,Qt 官方提供了 macdeployqt 工具,可以自动分析应用程序的依赖,并将所需的 Qt Frameworks 复制到应用程序包的 Contents/Frameworks 目录下。

    13.3.3 使用 macdeployqt 工具自动部署 Qt 依赖

    macdeployqt 是 Qt 官方提供的命令行工具,用于自动化部署 macOS 平台上的 Qt 应用程序依赖。它的功能与 Windows 平台上的 windeployqt 工具类似,但针对 macOS 平台和 Frameworks 进行了优化。macdeployqt 可以扫描应用程序包,分析其依赖的 Qt Frameworks 和平台插件,并将必要的 Frameworks 和插件复制到应用程序包的 Contents/Frameworks 目录下。

    macdeployqt 工具的使用方法

    1. 打开终端 (Terminal):在 macOS 中打开终端应用程序。

    2. 导航到应用程序包所在的目录:使用 cd 命令切换到应用程序包 (.app 文件夹) 所在的目录。例如:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 cd path/to/your/application/release
    1. 运行 macdeployqt 命令:在当前目录下直接运行 macdeployqt 命令,并指定应用程序包的路径作为参数。
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 macdeployqt MyApp.app

    macdeployqt 会自动扫描 MyApp.app 应用程序包,并复制所需的 Qt Frameworks 和平台插件到 MyApp.app/Contents/Frameworks/ 目录下。

    1. 检查部署结果macdeployqt 运行完成后,检查应用程序包 MyApp.appContents/Frameworks 目录。应该可以看到复制过来的 Qt Frameworks 文件夹(例如 QtCore.framework, QtGui.framework, QtWidgets.framework 等)。

    macdeployqt 常用选项

    -executable <可执行文件路径>:指定应用程序包中的可执行文件路径。如果 macdeployqt 无法自动找到可执行文件,可以使用此选项显式指定。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 macdeployqt -executable=MyApp.app/Contents/MacOS/MyApp MyApp.app

    -dmg:创建 DMG (Disk Image) 磁盘镜像文件。macdeployqt 会在完成 Frameworks 部署后,自动创建一个包含应用程序包的 DMG 文件,方便用户通过 DMG 文件安装应用程序。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 macdeployqt -dmg MyApp.app

    -codesign <代码签名标识>:在部署过程中进行代码签名。需要指定有效的代码签名标识 (Code Signing Identity)。代码签名是 macOS 应用程序发布的重要环节,后续章节将详细介绍。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 macdeployqt -codesign="Developer ID Application: Your Name (Team ID)" MyApp.app

    -verbose <级别>:设置详细输出级别。级别与 windeployqt 相同,可以是 0, 1, 2

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 macdeployqt -verbose 2 MyApp.app

    使用 macdeployqt 工具可以自动化 macOS 平台 Qt 应用程序的依赖部署过程,简化 Frameworks 复制操作,并支持创建 DMG 安装包和代码签名,为 macOS 应用程序的发布做好准备。

    13.3.4 代码签名 (Code Signing) 与公证 (Notarization)

    在 macOS 平台上,为了保障用户安全应用程序的可靠性,苹果公司推行了代码签名 (Code Signing)公证 (Notarization) 机制。对于发布的 macOS 应用程序,强烈建议进行代码签名和公证。

    代码签名 (Code Signing)

    代码签名是指使用数字证书 (Digital Certificate) 对应用程序进行签名,以验证应用程序的开发者身份,并保证应用程序在发布后没有被篡改。当用户运行已签名的应用程序时,macOS 系统会验证签名信息,如果签名有效,则可以确认应用程序来自可信的开发者,并且没有被恶意修改。

    代码签名的作用

    ▮▮▮▮⚝ 身份验证:证明应用程序的开发者身份,防止恶意软件冒充合法应用程序。
    ▮▮▮▮⚝ 完整性保护:确保应用程序在发布后没有被篡改,防止恶意代码注入。
    ▮▮▮▮⚝ 用户信任:提高用户对应用程序的信任度,减少安全警告。

    代码签名的步骤

    1. 获取开发者证书:需要加入苹果开发者计划 (Apple Developer Program),并从苹果开发者网站下载开发者证书。

    2. 安装证书到钥匙串 (Keychain Access):将下载的开发者证书导入到 macOS 的钥匙串访问 (Keychain Access) 应用程序中。

    3. 使用 codesign 工具进行签名:使用 macOS 自带的 codesign 命令行工具对应用程序包进行签名。需要指定开发者证书的标识 (Code Signing Identity)。

    例如,使用 "Developer ID Application: Your Name (Team ID)" 证书对 MyApp.app 进行代码签名的命令如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 codesign --deep --force --verify --verbose --sign "Developer ID Application: Your Name (Team ID)" MyApp.app

    ▮▮▮▮▮▮▮▮⚝ --deep: 递归签名应用程序包内的所有可执行文件和库文件。
    ▮▮▮▮▮▮▮▮⚝ --force: 强制签名,即使已签名也重新签名。
    ▮▮▮▮▮▮▮▮⚝ --verify: 签名后验证签名是否有效。
    ▮▮▮▮▮▮▮▮⚝ --verbose: 显示详细的签名过程信息。
    ▮▮▮▮▮▮▮▮⚝ --sign "...": 指定用于签名的代码签名标识。

    1. 验证签名:可以使用 codesign 工具验证应用程序的签名信息。
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 codesign --verify --verbose MyApp.app

    代码签名证书类型

    ▮▮▮▮⚝ Developer ID Application 证书: 用于发布到 App Store 之外的应用程序,例如通过网站下载或第三方分发平台发布的应用程序。使用 Developer ID 证书签名的应用程序,用户在首次运行时可能会看到安全警告,但可以选择信任并运行。

    ▮▮▮▮⚝ Apple Development 证书: 用于开发和测试阶段,通常用于在开发者自己的设备上运行应用程序。

    ▮▮▮▮⚝ Apple Distribution 证书: 用于发布到 App Store 的应用程序。

    公证 (Notarization)

    公证是苹果公司在 macOS Catalina (10.15) 及更高版本中引入的一项安全机制。公证是指将已签名的应用程序提交给苹果公司进行安全审查,苹果公司会对应用程序进行自动化的恶意软件扫描和安全检查。如果应用程序通过了公证,苹果公司会返回一个公证票据 (Notarization Ticket),并将该票据附加到应用程序包中。当用户运行已公证的应用程序时,macOS 系统会验证公证票据,如果票据有效,则可以进一步提高用户对应用程序安全性的信任,并减少或消除安全警告

    公证的作用

    ▮▮▮▮⚝ 安全审查:通过苹果公司的安全审查,降低应用程序包含恶意软件的风险。
    ▮▮▮▮⚝ 提升用户信任:用户更容易信任已公证的应用程序,减少安全警告,提升用户体验。
    ▮▮▮▮⚝ macOS 系统要求:在 macOS Catalina 及更高版本中,未公证的应用程序在首次运行时可能会遇到更严格的安全限制和警告。

    公证的步骤

    1. 代码签名必须先对应用程序进行代码签名,才能进行公证。

    2. 使用 xcrun altool 工具提交公证请求:使用 Xcode 命令行工具 xcrun altool 将已签名的应用程序包提交给苹果公司进行公证。需要提供苹果开发者账号的 App Store Connect 账号和密码。

    例如,提交 MyApp.app 进行公证的命令如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 xcrun altool --notarize-app --primary-bundle-id "com.example.myapp" --username "your_apple_id" --password "your_app_specific_password" --file MyApp.app

    ▮▮▮▮▮▮▮▮⚝ --notarize-app: 指定进行应用程序公证。
    ▮▮▮▮▮▮▮▮⚝ --primary-bundle-id: 应用程序的 Bundle Identifier (包标识符),与 Info.plist 文件中的 CFBundleIdentifier 对应。
    ▮▮▮▮▮▮▮▮⚝ --username: App Store Connect 账号。
    ▮▮▮▮▮▮▮▮⚝ --password: App Store Connect 账号的应用专用密码 (App-Specific Password)。强烈建议使用应用专用密码,而不是主密码
    ▮▮▮▮▮▮▮▮⚝ --file: 要公证的应用程序包路径。

    1. 轮询公证状态:公证过程需要一段时间,可以使用 xcrun altool 工具轮询公证状态,直到公证完成。
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 xcrun altool --notarization-info <请求 UUID> --username "your_apple_id" --password "your_app_specific_password"

    <请求 UUID> 是提交公证请求时返回的请求 UUID (Request UUID)。

    1. 附加公证票据 (Stapling):公证完成后,需要将苹果公司返回的公证票据附加到应用程序包中,这个过程称为 "Stapling"。可以使用 xcrun stapler 工具进行 Stapling。
    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 xcrun stapler staple MyApp.app

    代码签名和公证是 macOS 应用程序发布的最佳实践。代码签名验证开发者身份和应用程序完整性,公证进一步提升应用程序的安全性。对于希望在 macOS 平台上发布应用程序的开发者,代码签名和公证是强烈推荐的步骤。

    13.4 Linux 平台部署

    Linux 平台由于发行版众多,软件包管理方式多样,应用程序的部署相对复杂。本节将介绍 Linux 平台下 Qt GUI 应用程序的动态部署方法,包括 Qt 共享库 (Shared Libraries) 依赖处理、ldd 命令的使用以及通用 Linux 包格式的创建。

    13.4.1 动态部署:Qt 共享库 (Shared Libraries) 依赖

    在 Linux 平台上进行 Qt GUI 应用程序的动态部署,需要处理 Qt 共享库 (Shared Libraries) 依赖。与 Windows 平台上的 DLLs 和 macOS 平台上的 Frameworks 类似,Qt 在 Linux 平台上以 共享库 的形式提供动态库。例如,QtCore 模块对应 libQt6Core.so (Qt 6 版本),QtGui 模块对应 libQt6Gui.so 等等。共享库文件通常位于系统库目录,例如 /usr/lib//usr/local/lib/

    Linux 共享库的特点

    .so 后缀名:Linux 共享库文件通常以 .so (Shared Object) 为后缀名,例如 libQt6Core.so.6.5.0。后缀名中的数字部分表示库的版本号。

    库搜索路径:Linux 系统在运行时会按照一定的搜索路径 (Library Search Path) 查找共享库。常见的搜索路径包括 /lib/, /usr/lib/, /usr/local/lib/ 以及环境变量 LD_LIBRARY_PATH 中指定的目录。

    Qt 共享库部署

    对于 Qt GUI 应用程序,如果采用动态部署,需要确保目标系统上安装了应用程序所依赖的 Qt 共享库,或者将这些共享库与应用程序一起打包发布

    部署策略

    依赖系统 Qt 库

    最理想的情况是目标系统上已经安装了应用程序所需的 Qt 共享库。例如,许多 Linux 发行版(如 Ubuntu, Fedora, Debian 等)都提供了 Qt 软件包,可以通过系统的软件包管理器(例如 apt, dnf, yum 等)安装 Qt 库。如果目标用户已经安装了所需版本的 Qt 库,那么应用程序只需要依赖系统库即可,无需额外打包 Qt 库。

    优点

    ▮▮▮▮⚝ 包体积小:应用程序包体积非常小,只需要包含可执行文件和必要的资源文件。
    ▮▮▮▮⚝ 库共享:可以充分利用系统已安装的 Qt 库,节省磁盘空间和内存。
    ▮▮▮▮⚝ 系统维护:系统软件包管理器可以统一管理和更新 Qt 库,方便系统维护。

    缺点

    ▮▮▮▮⚝ 版本依赖:应用程序必须依赖特定版本的 Qt 库,如果目标系统上安装的 Qt 版本不兼容,可能会导致运行时问题。
    ▮▮▮▮⚝ 发行版差异:不同 Linux 发行版提供的 Qt 软件包版本和配置可能存在差异,可能需要针对不同发行版进行适配。
    ▮▮▮▮⚝ 用户安装:需要用户手动安装 Qt 库,对于非技术用户可能存在门槛。

    打包 Qt 共享库

    如果无法保证目标系统上安装了所需版本的 Qt 库,或者为了提高应用程序的独立性和可移植性,可以将应用程序所依赖的 Qt 共享库与应用程序一起打包发布

    打包方式

    ▮▮▮▮⚝ 复制共享库到应用程序目录:将应用程序所依赖的 Qt 共享库文件(例如 libQt6Core.so.6.5.0, libQt6Gui.so.6.5.0 等)复制到与应用程序可执行文件相同的目录下。运行时,操作系统会优先在应用程序所在目录查找共享库。

    ▮▮▮▮⚝ 设置 LD_LIBRARY_PATH 环境变量:将包含 Qt 共享库文件的目录添加到 LD_LIBRARY_PATH 环境变量中。运行时,操作系统会在 LD_LIBRARY_PATH 指定的目录中查找共享库。

    ▮▮▮▮⚝ 使用相对路径:在应用程序启动脚本中,设置共享库的相对路径,例如使用 rpathLD_PRELOAD 等技术。

    优点

    ▮▮▮▮⚝ 独立性:应用程序不依赖系统 Qt 库,可以在没有安装 Qt 的系统上运行。
    ▮▮▮▮⚝ 版本可控:应用程序可以携带特定版本的 Qt 库,避免版本冲突问题。
    ▮▮▮▮⚝ 可移植性:应用程序在不同 Linux 发行版之间的可移植性更好。

    缺点

    ▮▮▮▮⚝ 包体积大:应用程序包体积增大,需要包含 Qt 共享库文件。
    ▮▮▮▮⚝ 库更新维护:Qt 库的更新和维护需要由应用程序开发者负责。
    ▮▮▮▮⚝ 库冲突风险:如果用户系统上已存在相同名称但版本不兼容的共享库,可能会导致运行时问题。

    通常情况下,对于 Linux 平台 Qt GUI 应用程序,打包 Qt 共享库是更为常见的部署方式,尤其是在需要发布到多个不同 Linux 发行版时。

    13.4.2 使用 ldd 命令查看依赖库

    在 Linux 平台上,可以使用 ldd (List Dynamic Dependencies) 命令查看可执行文件或共享库所依赖的共享库列表ldd 命令可以帮助开发者分析应用程序的依赖关系,确定需要打包哪些 Qt 共享库。

    ldd 命令的使用方法

    在终端中,直接运行 ldd 命令,并指定可执行文件或共享库的路径作为参数。例如,查看应用程序 MyApp 的依赖库列表:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 ldd MyApp

    ldd 命令会输出 MyApp 所依赖的所有共享库及其路径。输出结果类似如下:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 linux-vdso.so.1 => (0x00007ffd779b6000)
    2 libQt6Widgets.so.6 => /path/to/Qt/lib/libQt6Widgets.so.6 (0x00007f7c8c7b8000)
    3 libQt6Gui.so.6 => /path/to/Qt/lib/libQt6Gui.so.6 (0x00007f7c8c100000)
    4 libQt6Core.so.6 => /path/to/Qt/lib/libQt6Core.so.6 (0x00007f7c8b800000)
    5 libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f7c8b478000)
    6 libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f7c8b170000)
    7 libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f7c8af58000)
    8 libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f7c8ab80000)
    9 libGL.so.1 => /usr/lib/x86_64-linux-gnu/libGL.so.1 (0x00007f7c8a900000)
    10 libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f7c8a6e0000)
    11 libz.so.1 => /lib/x86_64-linux-gnu/libz.so.1 (0x00007f7c8a4c0000)
    12 libicui18n.so.67 => /usr/lib/x86_64-linux-gnu/libicui18n.so.67 (0x00007f7c8a000000)
    13 libicuuc.so.67 => /usr/lib/x86_64-linux-gnu/libicuuc.so.67 (0x00007f7c89c00000)
    14 libicudata.so.67 => /usr/lib/x86_64-linux-gnu/libicudata.so.67 (0x00007f7c88100000)
    15 libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f7c87f00000)
    16 librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f7c87d00000)
    17 /lib64/ld-linux-x86-64.so.2 (0x00007f7c8d200000)

    ldd 的输出结果中,可以看到 MyApp 依赖的 Qt 共享库(例如 libQt6Widgets.so.6, libQt6Gui.so.6, libQt6Core.so.6)以及其他系统库(例如 libstdc++.so.6, libc.so.6 等)。对于 Qt 共享库,ldd 命令会显示其完整路径,例如 /path/to/Qt/lib/libQt6Widgets.so.6

    使用 ldd 命令分析依赖库的步骤

    1. 编译应用程序:首先需要编译生成 Linux 平台的可执行文件。

    2. 在终端中导航到可执行文件所在的目录:使用 cd 命令切换到可执行文件所在的目录。

    3. 运行 ldd 命令:在终端中运行 ldd MyApp (假设可执行文件名为 MyApp)。

    4. 分析 ldd 输出结果:查看 ldd 命令的输出结果,找到应用程序依赖的 Qt 共享库列表。通常以 libQt 开头的共享库都是 Qt 库。

    5. 复制 Qt 共享库:将 ldd 输出结果中列出的 Qt 共享库文件,从 Qt 安装目录(例如 /path/to/Qt/lib/复制到与应用程序可执行文件相同的目录下,或者其他合适的目录。

    通过 ldd 命令,开发者可以清晰地了解应用程序的共享库依赖关系,准确地确定需要打包哪些 Qt 共享库,从而确保应用程序在目标 Linux 系统上能够正常运行。

    13.4.3 创建 AppImage, Snap, Flatpak 等通用 Linux 包格式

    为了解决 Linux 发行版碎片化依赖库管理复杂性的问题,近年来出现了一些通用 Linux 包格式,例如 AppImage, Snap, Flatpak。这些通用包格式旨在创建一个自包含的应用程序包,包含应用程序及其所有依赖库,可以在不同的 Linux 发行版上通用运行,而无需考虑发行版之间的差异和依赖库版本问题。

    AppImage

    AppImage 是一个无须安装的 Linux 应用程序打包格式。AppImage 包是一个独立的可执行文件,包含了应用程序及其所有依赖库,用户只需下载 AppImage 文件,添加可执行权限,即可直接运行,无需解压或安装。

    AppImage 的特点

    ▮▮▮▮⚝ 自包含:AppImage 包包含了应用程序及其所有依赖库,运行时不依赖系统库。
    ▮▮▮▮⚝ 无须安装:用户只需下载 AppImage 文件即可直接运行,无需安装过程。
    ▮▮▮▮⚝ 跨发行版:AppImage 包可以在大多数 Linux 发行版上通用运行。
    ▮▮▮▮⚝ 单文件:AppImage 包是一个单独的文件,方便分发和管理。
    ▮▮▮▮⚝ 只读挂载:AppImage 包在运行时会被只读挂载,保证应用程序的完整性和安全性。

    创建 AppImage 包的工具

    ▮▮▮▮⚝ appimagetool: 官方提供的命令行工具,用于创建 AppImage 包。
    ▮▮▮▮⚝ linuxdeployqt: Qt 官方提供的工具,可以自动化部署 Qt 应用程序依赖,并生成 AppImage 包。linuxdeployqt 实际上是 appimagetool 的一个封装。

    使用 linuxdeployqt 创建 AppImage 包的步骤

    1. 安装 linuxdeployqt: 从 linuxdeployqt 的 GitHub 仓库 (https://github.com/linuxdeployqt/linuxdeployqt) 下载并安装 linuxdeployqt 工具。

    2. 编译应用程序:编译生成 Linux 平台的可执行文件。

    3. 运行 linuxdeployqt 命令:在终端中,导航到应用程序可执行文件所在的目录,运行 linuxdeployqt 命令,并指定可执行文件路径。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 linuxdeployqt MyApp

    linuxdeployqt 会自动分析应用程序依赖,复制 Qt 共享库和平台插件,并将应用程序打包成 AppImage 文件。生成的 AppImage 文件通常位于应用程序可执行文件所在的目录,例如 MyApp.AppImage

    1. 测试 AppImage 包:为生成的 AppImage 文件添加可执行权限 (chmod +x MyApp.AppImage),然后直接运行 AppImage 文件,测试应用程序是否正常运行。

    Snap

    Snap 是 Canonical 公司(Ubuntu 的开发商)推出的通用 Linux 包格式。Snap 包也是自包含的应用程序包,但与 AppImage 不同的是,Snap 包需要安装到系统中才能运行,并通过 Snap Store 进行分发和更新。

    Snap 的特点

    ▮▮▮▮⚝ 自包含:Snap 包包含了应用程序及其所有依赖库,运行时不依赖系统库。
    ▮▮▮▮⚝ 安装与卸载:Snap 包需要安装到系统中才能运行,可以通过 snap install 命令安装,通过 snap remove 命令卸载。
    ▮▮▮▮⚝ 自动更新:Snap Store 支持应用程序的自动更新。
    ▮▮▮▮⚝ 安全隔离:Snap 包在运行时被隔离在独立的沙箱 (Sandbox) 环境中,提高了安全性。
    ▮▮▮▮⚝ 跨发行版:Snap 包可以在支持 Snapd 的 Linux 发行版上通用运行。

    创建 Snap 包的工具

    ▮▮▮▮⚝ snapcraft: 官方提供的命令行工具,用于创建 Snap 包。

    创建 Snap 包的步骤

    1. 安装 snapcraft: 在 Linux 系统上安装 snapcraft 工具。

    2. 创建 snapcraft.yaml 文件:在应用程序项目根目录下创建一个名为 snapcraft.yaml 的 YAML 配置文件,描述 Snap 包的元数据、应用程序组件、依赖库、权限等信息。

    3. 使用 snapcraft 命令构建 Snap 包:在终端中,导航到应用程序项目根目录,运行 snapcraft 命令。snapcraft 会根据 snapcraft.yaml 文件构建 Snap 包,生成 .snap 文件。

    4. 安装和测试 Snap 包:使用 snap install --dangerous --devmode MyApp.snap 命令安装 Snap 包,并测试应用程序是否正常运行。

    Flatpak

    Flatpak 是一个由 freedesktop.org 社区主导开发的通用 Linux 包格式。Flatpak 包也旨在创建自包含的应用程序包,可以在不同的 Linux 发行版上通用运行,并通过 Flathub 应用商店进行分发和更新。

    Flatpak 的特点

    ▮▮▮▮⚝ 自包含:Flatpak 包包含了应用程序及其运行时环境和依赖库。
    ▮▮▮▮⚝ 安装与卸载:Flatpak 包需要安装到系统中才能运行,可以通过 flatpak install 命令安装,通过 flatpak uninstall 命令卸载。
    ▮▮▮▮⚝ 运行时共享:Flatpak 应用程序可以共享运行时环境 (Runtime),例如 GNOME Runtime 或 KDE Runtime,减少包体积和资源占用。
    ▮▮▮▮⚝ 自动更新:Flathub 应用商店支持应用程序的自动更新。
    ▮▮▮▮⚝ 安全隔离:Flatpak 应用程序在运行时被隔离在独立的沙箱环境中,提高了安全性。
    ▮▮▮▮⚝ 跨发行版:Flatpak 包可以在支持 Flatpak 的 Linux 发行版上通用运行。

    创建 Flatpak 包的工具

    ▮▮▮▮⚝ flatpak-builder: 官方提供的命令行工具,用于构建 Flatpak 包。

    创建 Flatpak 包的步骤

    1. 安装 flatpak-builder: 在 Linux 系统上安装 flatpak-builder 工具。

    2. 创建 manifest.json 文件:在应用程序项目根目录下创建一个名为 manifest.json 的 JSON 配置文件,描述 Flatpak 包的元数据、应用程序组件、构建步骤、运行时环境、权限等信息。

    3. 使用 flatpak-builder 命令构建 Flatpak 包:在终端中,导航到应用程序项目根目录,运行 flatpak-builder 命令,并指定构建目录和 manifest.json 文件路径。flatpak-builder 会根据 manifest.json 文件构建 Flatpak 包,生成 Flatpak 仓库。

    4. 安装和测试 Flatpak 包:使用 flatpak install --user path/to/flatpak-repository com.example.MyApp 命令安装 Flatpak 包,并测试应用程序是否正常运行。

    AppImage, Snap, Flatpak 这三种通用 Linux 包格式各有优缺点,开发者可以根据自己的需求和偏好选择合适的包格式。AppImage 以其无须安装、单文件的特点,适合快速分发和试用;Snap 和 Flatpak 以其自动更新、安全隔离的特性,以及 应用商店 的分发渠道,适合正式发布和长期维护的应用程序。对于 Qt GUI 应用程序,linuxdeployqt 工具可以方便地生成 AppImage 包,而 Snap 和 Flatpak 的创建过程相对复杂,需要编写配置文件和使用专门的构建工具。

    本章详细介绍了 Windows, macOS, Linux 三个平台下 Qt GUI 应用程序的部署与发布方法,包括依赖库处理、打包工具的使用以及安装包的创建。掌握这些部署技术,开发者可以将自己辛勤开发的 Qt GUI 应用程序顺利地交付给最终用户,让更多人体验到 Qt 的魅力。

    14. 实战案例:综合 GUI 应用开发

    本章通过实际案例,综合运用前面章节所学的知识,指导读者完成一个完整的 GUI 应用程序开发,例如一个简单的文本编辑器、图片浏览器或计算器。

    14.1 案例选择与需求分析

    本节选择一个合适的实战案例,并进行需求分析,明确应用程序的功能和界面设计。

    14.1.1 案例:简易文本编辑器

    选择简易文本编辑器作为实战案例。文本编辑器是一个经典的 GUI 应用程序,功能相对完整,但又不至于过于复杂,非常适合作为综合练习项目,能够涵盖 GUI 编程的多个方面,例如:

    ⚝ 窗口和控件的使用
    ⚝ 菜单栏和工具栏的构建
    ⚝ 布局管理
    ⚝ 事件处理
    ⚝ 文件操作
    ⚝ 文本编辑功能
    ⚝ 格式设置

    通过完成一个简易文本编辑器的开发,读者可以将前面章节学习到的 Qt GUI 编程知识融会贯通,并提升实际应用开发能力。

    14.1.2 功能需求分析:文件操作、文本编辑、格式设置

    针对简易文本编辑器,我们进行如下功能需求分析:

    ① 文件操作

    新建 (New):创建一个新的空白文档。
    打开 (Open):打开已有的文本文件(例如 .txt 文件)。
    保存 (Save):保存当前文档到文件,如果文档未保存过,则弹出“另存为”对话框。
    另存为 (Save As):将当前文档保存到指定的文件路径和文件名。
    退出 (Exit):关闭应用程序。

    ② 文本编辑

    文本输入:允许用户在编辑器中输入和编辑文本。
    复制 (Copy):复制选中的文本到剪贴板。
    粘贴 (Paste):从剪贴板粘贴文本到编辑器中。
    剪切 (Cut):剪切选中的文本到剪贴板。
    撤销 (Undo):撤销上一步操作。
    重做 (Redo):重做上一步被撤销的操作。
    全选 (Select All):选中编辑器中的所有文本。

    ③ 格式设置

    字体 (Font):允许用户选择字体类型。
    字号 (Font Size):允许用户选择字体大小。
    颜色 (Color):允许用户选择文本颜色。

    ④ 其他

    关于 (About):显示应用程序的基本信息,例如名称、版本等。

    这些功能覆盖了文本编辑器的基本操作,足以作为一个实战案例进行开发和学习。

    14.1.3 界面设计:菜单栏、工具栏、文本编辑区域

    为了实现上述功能,并提供友好的用户交互,我们设计简易文本编辑器的界面如下:

    ① 菜单栏 (Menu Bar)

    位于窗口顶部,包含以下菜单:

    文件 (File)
    ▮▮▮▮⚝ 新建 (New)
    ▮▮▮▮⚝ 打开 (Open)
    ▮▮▮▮⚝ 保存 (Save)
    ▮▮▮▮⚝ 另存为 (Save As)
    ▮▮▮▮⚝ 退出 (Exit)
    编辑 (Edit)
    ▮▮▮▮⚝ 撤销 (Undo)
    ▮▮▮▮⚝ 重做 (Redo)
    ▮▮▮▮⚝ 剪切 (Cut)
    ▮▮▮▮⚝ 复制 (Copy)
    ▮▮▮▮⚝ 粘贴 (Paste)
    ▮▮▮▮⚝ 全选 (Select All)
    格式 (Format)
    ▮▮▮▮⚝ 字体 (Font)
    帮助 (Help)
    ▮▮▮▮⚝ 关于 (About)

    ② 工具栏 (Tool Bar)

    位于菜单栏下方,提供常用功能的快捷按钮,例如:

    ⚝ 新建 (New)
    ⚝ 打开 (Open)
    ⚝ 保存 (Save)
    ⚝ 剪切 (Cut)
    ⚝ 复制 (Copy)
    ⚝ 粘贴 (Paste)
    ⚝ 字体 (Font)

    工具栏可以根据需要添加更多常用操作的按钮,这里列出的是一些基本选项。

    ③ 文本编辑区域 (Text Edit Area)

    占据窗口的主要区域,用于显示和编辑文本内容。我们将使用 QTextEdit 控件来实现文本编辑区域,它支持富文本编辑,能够满足我们基本的文本编辑需求。

    ④ 状态栏 (Status Bar) (可选)

    位于窗口底部 (可选),可以用于显示一些状态信息,例如当前文件路径、光标位置等。在本简易文本编辑器案例中,状态栏可以作为扩展功能,基本功能实现中可以先不考虑。

    通过以上界面设计,我们为简易文本编辑器搭建了一个清晰、易用的操作框架,接下来我们将开始项目搭建和界面布局的实现。

    14.2 项目搭建与界面布局

    本节将搭建项目框架,创建 Qt 项目,并完成界面布局,包括窗口、菜单栏、工具栏和文本编辑控件的添加和布局。

    14.2.1 创建 Qt Widgets Application 项目

    首先,使用 Qt Creator 创建一个新的 Qt Widgets Application 项目。步骤如下:

    1. 打开 Qt Creator,点击 File (文件) -> New File or Project... (新建文件或项目...)
    2. 在 “New Project (新建项目)” 对话框中,选择 Qt Widgets Application,然后点击 Choose... (选择...)
    3. 在 “Location (位置)” 页面,设置项目名称 (例如 SimpleTextEditor) 和项目存放路径,然后点击 Next (下一步)
    4. 在 “Configure Project (配置项目)” 页面,可以根据需要选择构建套件 (Kit),一般默认即可,然后点击 Next (下一步)
    5. 在 “Class Information (类信息)” 页面,设置类名 (例如 MainWindow)、基类 (默认 QMainWindow) 等信息,然后点击 Next (下一步)
    6. 在 “Project Management (项目管理)” 页面,可以添加版本控制等,一般默认即可,然后点击 Finish (完成)

    创建项目后,Qt Creator 会自动生成项目文件,包括 .pro 项目文件、头文件 (.h)、源文件 (.cpp)、UI 文件 (.ui) 等。我们的界面布局主要在 mainwindow.ui 文件中进行(如果选择使用 Qt Designer),或者在 mainwindow.cpp 中通过代码进行布局。

    14.2.2 使用 Qt Designer 设计界面 (可选) 或代码布局

    Qt 提供了两种方式进行界面布局:Qt Designer 可视化设计器和代码布局

    ① 使用 Qt Designer 设计界面 (推荐初学者)

    ⚝ 双击项目文件列表中的 mainwindow.ui 文件,打开 Qt Designer。
    ⚝ 在 Qt Designer 中,从 Widget Box (控件箱) 中拖拽所需的控件到主窗口 (MainWindow) 上。
    ▮▮▮▮⚝ 首先,添加 Menu Bar (菜单栏),Qt Designer 会自动在窗口顶部创建菜单栏区域。
    ▮▮▮▮⚝ 然后,添加 Tool Bar (工具栏),可以将其停靠在菜单栏下方。
    ▮▮▮▮⚝ 接着,添加 Text Edit (文本编辑) 控件,并调整其大小以占据窗口的主要区域。
    ⚝ 在 Object Inspector (对象查看器) 中,可以查看和修改控件的属性,例如 objectName (对象名称)text (文本) 等。
    ⚝ 在 Signal & Slots Editor (信号与槽编辑器) 中,可以配置控件的信号与槽连接,实现事件响应。
    ⚝ 完成界面设计后,保存 mainwindow.ui 文件。

    ② 代码布局

    如果不使用 Qt Designer,也可以完全通过 C++ 代码进行界面布局。在 mainwindow.cpp 的构造函数中进行控件的创建和布局:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include "mainwindow.h"
    2 #include <QMenuBar>
    3 #include <QToolBar>
    4 #include <QTextEdit>
    5 #include <QVBoxLayout> // 垂直布局管理器
    6
    7 MainWindow::MainWindow(QWidget *parent)
    8 : QMainWindow(parent)
    9 {
    10 // 创建菜单栏
    11 QMenuBar *menuBar = new QMenuBar(this);
    12 QMenu *fileMenu = menuBar->addMenu(tr("文件(File)"));
    13 QMenu *editMenu = menuBar->addMenu(tr("编辑(Edit)"));
    14 QMenu *formatMenu = menuBar->addMenu(tr("格式(Format)"));
    15 QMenu *helpMenu = menuBar->addMenu(tr("帮助(Help)"));
    16 setMenuBar(menuBar); // 设置主窗口的菜单栏
    17
    18 // 创建工具栏
    19 QToolBar *toolBar = new QToolBar(this);
    20 addToolBar(toolBar); // 添加工具栏到主窗口
    21
    22 // 创建文本编辑区域
    23 QTextEdit *textEdit = new QTextEdit(this);
    24 setCentralWidget(textEdit); // 设置中心控件为文本编辑区域
    25
    26 // 设置窗口标题
    27 setWindowTitle(tr("简易文本编辑器"));
    28 }
    29
    30 MainWindow::~MainWindow()
    31 {
    32 }

    上述代码创建了菜单栏、工具栏和文本编辑区域,并使用 setMenuBar()addToolBar()setCentralWidget() 将它们添加到主窗口。 为了实现更精细的布局,例如使用布局管理器,可以在代码中进一步添加布局管理器,并将控件添加到布局中。例如,可以使用 QVBoxLayout (垂直布局管理器) 来管理工具栏和文本编辑区域的垂直排列。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // ... (前面的代码)
    2
    3 // 创建主布局
    4 QVBoxLayout *mainLayout = new QVBoxLayout;
    5 mainLayout->addWidget(toolBar); // 工具栏添加到布局
    6 mainLayout->addWidget(textEdit); // 文本编辑区域添加到布局
    7
    8 QWidget *centralWidget = new QWidget(this); // 创建一个QWidget作为中心控件
    9 centralWidget->setLayout(mainLayout); // 设置中心控件的布局
    10 setCentralWidget(centralWidget); // 将QWidget设置为主窗口的中心控件
    11
    12 // ... (后面的代码)

    虽然代码布局更灵活,但对于初学者来说,使用 Qt Designer 可视化设计界面通常更直观、更高效。本书后续章节将主要以 Qt Designer 设计界面为例进行讲解,代码布局作为可选方式。

    14.2.3 实现主窗口布局:QMainWindow, QMenuBar, QToolBar, QTextEdit

    无论使用 Qt Designer 还是代码布局,最终都需要实现包含 QMainWindow (主窗口)、QMenuBar (菜单栏)、QToolBar (工具栏)、QTextEdit (文本编辑区域) 的主窗口布局。

    ① QMainWindow (主窗口)

    QMainWindow 是 Qt 中用于创建主窗口应用程序的基类。它提供了窗口的基本框架,包括菜单栏、工具栏、状态栏、停靠窗口和中心控件区域。我们的文本编辑器的主窗口类 MainWindow 继承自 QMainWindow

    ② QMenuBar (菜单栏)

    QMenuBar 用于在窗口顶部创建菜单栏。通过 QMainWindow::menuBar() 方法可以获取主窗口的菜单栏对象,然后使用 QMenuBar::addMenu() 添加菜单,使用 QMenu::addAction() 添加菜单项。

    在 Qt Designer 中添加菜单栏,直接拖拽 Menu Bar 控件到窗口即可。在代码中创建菜单栏,如前述代码示例所示。

    ③ QToolBar (工具栏)

    QToolBar 用于在窗口中创建工具栏,通常位于菜单栏下方,提供常用功能的快捷按钮。通过 QMainWindow::addToolBar() 方法可以添加工具栏到主窗口。 可以使用 QToolBar::addAction() 添加工具栏按钮,按钮通常关联一个 QAction 对象,用于定义按钮的文本、图标、快捷键和信号槽连接。

    在 Qt Designer 中添加工具栏,拖拽 Tool Bar 控件到窗口,然后可以在工具栏中添加 Push Button 等控件作为工具按钮。在代码中创建工具栏,如前述代码示例所示,并可以通过 QAction 添加工具栏按钮。

    ④ QTextEdit (文本编辑区域)

    QTextEdit 是 Qt 中用于显示和编辑多行文本的控件,支持富文本编辑。我们将使用 QTextEdit 作为文本编辑器的主要文本输入和显示区域。通过 QMainWindow::setCentralWidget() 方法将 QTextEdit 设置为主窗口的中心控件,使其占据窗口的主要区域。

    在 Qt Designer 中添加 QTextEdit,拖拽 Text Edit 控件到窗口,并调整其大小以占据窗口中心区域。在代码中创建 QTextEdit,如前述代码示例所示,并使用 setCentralWidget() 设置中心控件。

    完成以上步骤后,我们就搭建了简易文本编辑器的基本界面框架,包括菜单栏、工具栏和文本编辑区域。接下来,我们将开始实现各项功能和事件处理。

    14.3 功能实现与事件处理

    本节将实现文本编辑器的各项功能,包括文件的新建、打开、保存、另存为、复制、粘贴、剪切、字体设置等,并处理相应的事件。

    14.3.1 文件操作功能实现:新建、打开、保存、另存为

    ① 新建 (New)

    功能:创建一个新的空白文档。
    实现
    ▮▮▮▮⚝ 在 “文件 (File)” 菜单下添加 “新建 (New)” 菜单项 (例如 actionNew),并在工具栏添加对应的 “新建” 按钮 (例如 toolButtonNew)。
    ▮▮▮▮⚝ 连接 “新建” 菜单项和按钮的 triggered() 信号到一个槽函数 (例如 MainWindow::newFile())。
    ▮▮▮▮⚝ 在 newFile() 槽函数中,清空 QTextEdit 的内容 (QTextEdit::clear()),并将当前文件路径设置为空,表示为新文档。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MainWindow::newFile()
    2 {
    3 textEdit->clear(); // 清空文本编辑区域
    4 currentFilePath.clear(); // 清空当前文件路径
    5 setWindowTitle(tr("新建文档 - 简易文本编辑器")); // 更新窗口标题
    6 }

    ② 打开 (Open)

    功能:打开已有的文本文件。
    实现
    ▮▮▮▮⚝ 在 “文件 (File)” 菜单下添加 “打开 (Open)” 菜单项 (例如 actionOpen),并在工具栏添加对应的 “打开” 按钮 (例如 toolButtonOpen)。
    ▮▮▮▮⚝ 连接 “打开” 菜单项和按钮的 triggered() 信号到一个槽函数 (例如 MainWindow::openFile())。
    ▮▮▮▮⚝ 在 openFile() 槽函数中,使用 QFileDialog::getOpenFileName() 打开文件对话框,让用户选择要打开的文本文件。
    ▮▮▮▮⚝ 如果用户选择了文件,则读取文件内容 (QFile, QTextStream) 并显示在 QTextEdit 中,同时记录当前文件路径。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QFileDialog>
    2 #include <QFile>
    3 #include <QTextStream>
    4
    5 void MainWindow::openFile()
    6 {
    7 QString filePath = QFileDialog::getOpenFileName(this, tr("打开文件"),
    8 "", tr("文本文件 (*.txt);;所有文件 (*)"));
    9 if (!filePath.isEmpty()) {
    10 QFile file(filePath);
    11 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    12 QTextStream in(&file);
    13 textEdit->setText(in.readAll()); // 读取文件内容并显示
    14 currentFilePath = filePath; // 记录当前文件路径
    15 setWindowTitle(QFileInfo(filePath).fileName() + tr(" - 简易文本编辑器")); // 更新窗口标题
    16 file.close();
    17 }
    18 }
    19 }

    ③ 保存 (Save)

    功能:保存当前文档到文件,如果文档未保存过,则弹出“另存为”对话框。
    实现
    ▮▮▮▮⚝ 在 “文件 (File)” 菜单下添加 “保存 (Save)” 菜单项 (例如 actionSave),并在工具栏添加对应的 “保存” 按钮 (例如 toolButtonSave)。
    ▮▮▮▮⚝ 连接 “保存” 菜单项和按钮的 triggered() 信号到一个槽函数 (例如 MainWindow::saveFile())。
    ▮▮▮▮⚝ 在 saveFile() 槽函数中,判断 currentFilePath 是否为空。
    ▮▮▮▮▮▮▮▮⚝ 如果为空,表示是新文档,调用 saveFileAs() 函数执行“另存为”操作。
    ▮▮▮▮▮▮▮▮⚝ 如果不为空,表示已打开或已保存过的文档,直接将 QTextEdit 的内容写入到 currentFilePath 指定的文件中 (QFile, QTextStream)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MainWindow::saveFile()
    2 {
    3 if (currentFilePath.isEmpty()) {
    4 saveFileAs(); // 如果是新文档,执行另存为
    5 } else {
    6 QFile file(currentFilePath);
    7 if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
    8 QTextStream out(&file);
    9 out << textEdit->toPlainText(); // 将文本编辑区域内容写入文件
    10 file.close();
    11 }
    12 }
    13 }

    ④ 另存为 (Save As)

    功能:将当前文档保存到指定的文件路径和文件名。
    实现
    ▮▮▮▮⚝ 在 “文件 (File)” 菜单下添加 “另存为 (Save As)” 菜单项 (例如 actionSaveAs)。
    ▮▮▮▮⚝ 连接 “另存为” 菜单项的 triggered() 信号到一个槽函数 (例如 MainWindow::saveFileAs())。
    ▮▮▮▮⚝ 在 saveFileAs() 槽函数中,使用 QFileDialog::getSaveFileName() 打开“另存为”文件对话框,让用户选择保存的文件路径和文件名。
    ▮▮▮▮⚝ 如果用户选择了文件路径,则将 QTextEdit 的内容写入到指定的文件中 (QFile, QTextStream),并更新 currentFilePath 为新的文件路径。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MainWindow::saveFileAs()
    2 {
    3 QString filePath = QFileDialog::getSaveFileName(this, tr("另存为"),
    4 "", tr("文本文件 (*.txt);;所有文件 (*)"));
    5 if (!filePath.isEmpty()) {
    6 QFile file(filePath);
    7 if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
    8 QTextStream out(&file);
    9 out << textEdit->toPlainText(); // 将文本编辑区域内容写入文件
    10 currentFilePath = filePath; // 更新当前文件路径
    11 setWindowTitle(QFileInfo(filePath).fileName() + tr(" - 简易文本编辑器")); // 更新窗口标题
    12 file.close();
    13 }
    14 }
    15 }

    ⑤ 退出 (Exit)

    功能:关闭应用程序。
    实现
    ▮▮▮▮⚝ 在 “文件 (File)” 菜单下添加 “退出 (Exit)” 菜单项 (例如 actionExit)。
    ▮▮▮▮⚝ 连接 “退出” 菜单项的 triggered() 信号到一个槽函数 (例如 MainWindow::exitApp())。
    ▮▮▮▮⚝ 在 exitApp() 槽函数中,调用 QApplication::quit() 退出应用程序。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QApplication>
    2
    3 void MainWindow::exitApp()
    4 {
    5 QApplication::quit(); // 退出应用程序
    6 }

    在 Qt Designer 中设计菜单栏和工具栏,并添加相应的菜单项和工具按钮,然后在 MainWindow 类中添加上述槽函数,并将菜单项和按钮的 triggered() 信号连接到对应的槽函数。

    14.3.2 文本编辑功能实现:复制、粘贴、剪切、撤销、重做

    QTextEdit 控件本身已经提供了基本的文本编辑功能,例如文本输入、选择、复制、粘贴、剪切、撤销、重做等。我们可以直接利用 QTextEdit 的内置功能,并通过菜单项和工具栏按钮来触发这些功能。

    ① 复制 (Copy)

    功能:复制选中的文本到剪贴板。
    实现
    ▮▮▮▮⚝ 在 “编辑 (Edit)” 菜单下添加 “复制 (Copy)” 菜单项 (例如 actionCopy),并在工具栏添加对应的 “复制” 按钮 (例如 toolButtonCopy)。
    ▮▮▮▮⚝ 连接 “复制” 菜单项和按钮的 triggered() 信号到 QTextEdit::copy() 槽函数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(actionCopy, &QAction::triggered, textEdit, &QTextEdit::copy);
    2 // 或者在Qt Designer 的信号槽编辑器中直接连接

    ② 粘贴 (Paste)

    功能:从剪贴板粘贴文本到编辑器中。
    实现
    ▮▮▮▮⚝ 在 “编辑 (Edit)” 菜单下添加 “粘贴 (Paste)” 菜单项 (例如 actionPaste),并在工具栏添加对应的 “粘贴” 按钮 (例如 toolButtonPaste)。
    ▮▮▮▮⚝ 连接 “粘贴” 菜单项和按钮的 triggered() 信号到 QTextEdit::paste() 槽函数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(actionPaste, &QAction::triggered, textEdit, &QTextEdit::paste);
    2 // 或者在Qt Designer 的信号槽编辑器中直接连接

    ③ 剪切 (Cut)

    功能:剪切选中的文本到剪贴板。
    实现
    ▮▮▮▮⚝ 在 “编辑 (Edit)” 菜单下添加 “剪切 (Cut)” 菜单项 (例如 actionCut),并在工具栏添加对应的 “剪切” 按钮 (例如 toolButtonCut)。
    ▮▮▮▮⚝ 连接 “剪切” 菜单项和按钮的 triggered() 信号到 QTextEdit::cut() 槽函数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(actionCut, &QAction::triggered, textEdit, &QTextEdit::cut);
    2 // 或者在Qt Designer 的信号槽编辑器中直接连接

    ④ 撤销 (Undo)

    功能:撤销上一步操作。
    实现
    ▮▮▮▮⚝ 在 “编辑 (Edit)” 菜单下添加 “撤销 (Undo)” 菜单项 (例如 actionUndo)。
    ▮▮▮▮⚝ 连接 “撤销” 菜单项的 triggered() 信号到 QTextEdit::undo() 槽函数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(actionUndo, &QAction::triggered, textEdit, &QTextEdit::undo);
    2 // 或者在Qt Designer 的信号槽编辑器中直接连接

    ⑤ 重做 (Redo)

    功能:重做上一步被撤销的操作。
    实现
    ▮▮▮▮⚝ 在 “编辑 (Edit)” 菜单下添加 “重做 (Redo)” 菜单项 (例如 actionRedo)。
    ▮▮▮▮⚝ 连接 “重做” 菜单项的 triggered() 信号到 QTextEdit::redo() 槽函数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(actionRedo, &QAction::triggered, textEdit, &QTextEdit::redo);
    2 // 或者在Qt Designer 的信号槽编辑器中直接连接

    ⑥ 全选 (Select All)

    功能:选中编辑器中的所有文本。
    实现
    ▮▮▮▮⚝ 在 “编辑 (Edit)” 菜单下添加 “全选 (Select All)” 菜单项 (例如 actionSelectAll)。
    ▮▮▮▮⚝ 连接 “全选” 菜单项的 triggered() 信号到 QTextEdit::selectAll() 槽函数。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(actionSelectAll, &QAction::triggered, textEdit, &QTextEdit::selectAll);
    2 // 或者在Qt Designer 的信号槽编辑器中直接连接

    通过连接菜单项和工具栏按钮的 triggered() 信号到 QTextEdit 相应的槽函数,我们可以快速实现文本编辑的基本功能,无需编写额外的代码逻辑,充分利用了 Qt 控件的内置功能。

    14.3.3 格式设置功能实现:字体、字号、颜色

    ① 字体 (Font)

    功能:允许用户选择字体类型。
    实现
    ▮▮▮▮⚝ 在 “格式 (Format)” 菜单下添加 “字体 (Font)” 菜单项 (例如 actionFont),并在工具栏添加对应的 “字体” 按钮 (例如 toolButtonFont)。
    ▮▮▮▮⚝ 连接 “字体” 菜单项和按钮的 triggered() 信号到一个槽函数 (例如 MainWindow::setFont())。
    ▮▮▮▮⚝ 在 setFont() 槽函数中,使用 QFontDialog::getFont() 打开字体对话框,让用户选择字体。
    ▮▮▮▮⚝ 如果用户选择了字体,则使用 QTextEdit::setFont() 设置 QTextEdit 的字体。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QFontDialog>
    2
    3 void MainWindow::setFont()
    4 {
    5 bool ok;
    6 QFont font = QFontDialog::getFont(&ok, textEdit->font(), this, tr("选择字体"));
    7 if (ok) {
    8 textEdit->setFont(font); // 设置文本编辑区域字体
    9 }
    10 }

    ② 字号 (Font Size)

    功能:允许用户选择字体大小。
    实现
    ▮▮▮▮⚝ 字号的设置可以通过多种方式实现,例如使用下拉框 (QComboBox) 或微调框 (QSpinBox) 在工具栏上提供字号选择。
    ▮▮▮▮⚝ 这里我们以使用 QComboBox 下拉框为例。在工具栏上添加一个 QComboBox 控件 (例如 fontSizeComboBox),并预设一些常用的字号选项 (例如 8, 9, 10, 11, 12, 14, 16, 18, 20, 24, 36, 48)。
    ▮▮▮▮⚝ 连接 fontSizeComboBoxcurrentIndexChanged(int) 信号到一个槽函数 (例如 MainWindow::setFontSize(int index))。
    ▮▮▮▮⚝ 在 setFontSize(int index) 槽函数中,获取选中的字号 (从 fontSizeComboBox 的文本中解析出字号数值),并使用 QFont::setPointSize() 设置 QTextEdit 的字体大小。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MainWindow::setFontSize(int index)
    2 {
    3 QString fontSizeText = fontSizeComboBox->itemText(index);
    4 int fontSize = fontSizeText.toInt();
    5 if (fontSize > 0) {
    6 QFont font = textEdit->font();
    7 font.setPointSize(fontSize);
    8 textEdit->setFont(font); // 设置文本编辑区域字体大小
    9 }
    10 }

    MainWindow 类的构造函数中,初始化 fontSizeComboBox 的选项:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 fontSizeComboBox = new QComboBox(this);
    2 fontSizeComboBox->addItems({"8", "9", "10", "11", "12", "14", "16", "18", "20", "24", "36", "48"});
    3 toolBar->addWidget(fontSizeComboBox); // 将字号下拉框添加到工具栏
    4 connect(fontSizeComboBox, &QComboBox::currentIndexChanged, this, &MainWindow::setFontSize);

    ③ 颜色 (Color)

    功能:允许用户选择文本颜色。
    实现
    ▮▮▮▮⚝ 在 “格式 (Format)” 菜单下添加 “颜色 (Color)” 菜单项 (例如 actionColor),并在工具栏添加对应的 “颜色” 按钮 (例如 toolButtonColor)。
    ▮▮▮▮⚝ 连接 “颜色” 菜单项和按钮的 triggered() 信号到一个槽函数 (例如 MainWindow::setColor())。
    ▮▮▮▮⚝ 在 setColor() 槽函数中,使用 QColorDialog::getColor() 打开颜色对话框,让用户选择颜色。
    ▮▮▮▮⚝ 如果用户选择了颜色,则使用 QTextCharFormat 设置 QTextEdit 的文本字符格式,包括颜色,并应用到 QTextEdit 的当前光标位置 (QTextCursor)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QColorDialog>
    2 #include <QTextCharFormat>
    3 #include <QTextCursor>
    4
    5 void MainWindow::setColor()
    6 {
    7 QColor color = QColorDialog::getColor(Qt::black, this, tr("选择颜色"));
    8 if (color.isValid()) {
    9 QTextCharFormat fmt;
    10 fmt.setForeground(color); // 设置前景色 (文本颜色)
    11 textEdit->mergeCurrentCharFormat(fmt); // 应用到当前光标位置
    12 }
    13 }

    通过上述方法,我们实现了简易文本编辑器的格式设置功能,包括字体、字号和颜色。用户可以通过菜单项、工具栏按钮和下拉框来调整文本的显示样式。

    14.3.4 菜单项与工具栏按钮的事件响应

    为了使菜单项和工具栏按钮能够响应用户的点击操作,我们需要将它们的 triggered() 信号连接到相应的槽函数。

    ① 在 Qt Designer 中连接信号与槽

    ⚝ 在 Qt Designer 中,打开 mainwindow.ui 文件。
    ⚝ 点击 Edit (编辑) -> Edit Signals/Slots (编辑信号/槽),进入信号槽编辑模式。
    ⚝ 选中要连接信号的控件 (例如 “新建” 菜单项 actionNew),按住鼠标左键拖拽到要接收信号的对象 (例如 MainWindow 窗口)。
    ⚝ 释放鼠标后,弹出 “Configure Connection (配置连接)” 对话框。
    ⚝ 在 “Signal (信号)” 下拉框中选择 triggered() 信号。
    ⚝ 在 “Slot (槽)” 下拉框中选择对应的槽函数 (例如 newFile(),如果槽函数还未定义,需要先在 mainwindow.h 中声明,然后在 mainwindow.cpp 中实现)。
    ⚝ 点击 OK (确定) 完成连接。
    ⚝ 重复以上步骤,连接其他菜单项和工具栏按钮的 triggered() 信号到相应的槽函数。

    ② 在代码中连接信号与槽

    MainWindow 类的构造函数中,使用 QObject::connect() 函数进行信号与槽的连接。例如,连接 “新建” 菜单项 actionNewtriggered() 信号到 MainWindow::newFile() 槽函数:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(actionNew, &QAction::triggered, this, &MainWindow::newFile);

    其中,actionNew 是信号发送者 (菜单项对象),triggered 是信号,this 是信号接收者 (MainWindow 对象),&MainWindow::newFile 是槽函数。

    对于工具栏按钮,连接方式类似:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 connect(toolButtonNew, &QToolButton::clicked, this, &MainWindow::newFile); // 工具按钮使用 clicked() 信号

    需要注意的是,QToolButton 使用 clicked() 信号,而 QAction (菜单项和工具栏按钮通常关联 QAction) 使用 triggered() 信号。

    通过信号与槽的连接,当用户点击菜单项或工具栏按钮时,相应的槽函数会被调用,从而实现应用程序的功能逻辑。

    14.4 测试与优化

    本节将对完成的文本编辑器进行测试,修复 Bug,并进行性能优化,提高程序的稳定性和用户体验。

    14.4.1 功能测试与 Bug 修复

    完成简易文本编辑器的功能实现后,需要进行全面的功能测试,以确保各项功能符合需求,并发现和修复 Bug (程序缺陷)。

    ① 文件操作测试

    新建:测试新建功能是否能创建空白文档,窗口标题是否正确更新。
    打开:测试打开功能是否能正确打开文本文件,文件内容是否正确显示,窗口标题是否正确更新。测试打开不存在的文件或非文本文件的情况,程序是否能正确处理。
    保存:测试保存功能是否能正确保存已修改的文档,保存后的文件内容是否与编辑器中一致。
    另存为:测试另存为功能是否能将文档保存到指定路径,文件内容是否正确,窗口标题是否正确更新。
    退出:测试退出功能是否能正常退出应用程序。

    ② 文本编辑功能测试

    文本输入:测试是否能正常输入各种字符,包括英文、中文、数字、符号等。
    复制:测试复制功能是否能正确复制选中文本到剪贴板,粘贴后内容是否正确。
    粘贴:测试粘贴功能是否能从剪贴板正确粘贴文本到编辑器中。
    剪切:测试剪切功能是否能正确剪切选中文本到剪贴板,粘贴后内容是否正确,剪切后原位置文本是否消失。
    撤销:测试撤销功能是否能正确撤销上一步操作,多次撤销是否正常。
    重做:测试重做功能是否能正确重做被撤销的操作,多次重做是否正常。
    全选:测试全选功能是否能正确选中所有文本。

    ③ 格式设置功能测试

    字体:测试字体设置功能是否能正确打开字体对话框,选择字体后编辑器字体是否正确改变。
    字号:测试字号设置功能是否能通过下拉框选择字号,选择字号后编辑器字号是否正确改变。
    颜色:测试颜色设置功能是否能正确打开颜色对话框,选择颜色后编辑器文本颜色是否正确改变。

    ④ 边界条件和异常测试

    超大文件打开:测试打开超大文本文件时,程序是否能正常响应,内存占用是否合理。
    连续操作:进行连续的新建、打开、保存、编辑等操作,测试程序是否稳定,是否存在内存泄漏或其他异常。
    文件读写错误:模拟文件读写错误 (例如文件权限不足、磁盘空间不足等),测试程序是否能正确处理错误情况,并给出友好的提示。

    在测试过程中,记录发现的 Bug,并根据 Bug 的具体情况进行代码调试和修复。Qt Creator 提供了强大的调试工具,例如断点调试、变量查看等,可以帮助开发者快速定位和解决 Bug。

    14.4.2 性能优化:启动速度、内存占用、响应速度

    在功能测试通过后,可以对简易文本编辑器进行性能优化,提高程序的启动速度、降低内存占用、提升响应速度,从而提升用户体验。

    ① 启动速度优化

    延迟初始化:将一些非必要的初始化操作延迟到程序启动后或需要使用时再进行,例如某些模块的加载、资源的创建等。
    减少启动时的计算量:避免在启动时进行大量的计算操作,例如复杂的算法、大数据量的加载等。
    使用更快的资源加载方式:例如使用异步加载资源、使用内存映射文件等。
    代码优化:检查启动阶段的代码,是否存在可以优化的部分,例如减少循环次数、优化算法复杂度等。

    ② 内存占用优化

    对象池:对于频繁创建和销毁的对象,可以使用对象池进行管理,减少内存分配和释放的开销。
    资源管理:合理管理程序使用的资源,例如图片、字体、文件句柄等,及时释放不再使用的资源,避免内存泄漏。
    数据结构优化:选择合适的数据结构,例如使用更节省内存的数据结构、使用共享数据等。
    代码优化:检查代码中是否存在内存浪费的情况,例如创建了不必要的对象、使用了过大的数据结构等。

    ③ 响应速度优化

    多线程:对于耗时的操作 (例如文件读写、网络请求等),可以使用多线程技术,将耗时操作放在后台线程执行,避免阻塞主线程,保持界面响应流畅。
    异步操作:对于某些可以异步执行的操作,可以使用异步操作,例如异步文件读写、异步网络请求等,提高程序的并发性和响应速度。
    算法优化:对于计算密集型的操作,可以优化算法,降低算法复杂度,提高计算效率。
    缓存机制:对于一些可以缓存的数据,可以使用缓存机制,减少重复计算或重复加载的开销。
    界面渲染优化:减少不必要的界面重绘,优化界面渲染逻辑,提高界面渲染效率。

    可以使用性能分析工具 (例如 Qt Creator 自带的性能分析器、Valgrind 等) 来分析程序的性能瓶颈,找出需要优化的部分,并进行针对性的优化。

    14.4.3 用户体验优化:界面美观、操作流畅

    除了功能和性能,用户体验也是 GUI 应用程序的重要方面。用户体验优化主要包括界面美观和操作流畅两个方面。

    ① 界面美观

    统一的界面风格:使用 Qt Style Sheets 或平台原生风格,统一应用程序的界面风格,使其更符合用户的使用习惯。
    合理的颜色搭配:选择合适的颜色搭配,使界面色彩协调、视觉舒适。
    清晰的图标和文字:使用清晰、易于理解的图标和文字,方便用户操作和理解。
    合理的控件布局:采用合理的布局管理器,使界面控件排列整齐、布局紧凑、易于操作。
    界面细节优化:例如窗口边框、控件间距、字体大小、按钮样式等细节的调整,提升界面的整体美观度。

    ② 操作流畅

    快速响应:确保用户操作能得到及时响应,避免长时间的等待或卡顿现象。
    平滑的动画效果:在适当的地方添加平滑的动画效果 (例如窗口切换动画、控件状态切换动画等),提升界面的生动性和流畅感。
    快捷键支持:为常用操作提供快捷键支持,提高操作效率。
    友好的提示信息:在用户操作出错或需要用户确认时,给出友好的提示信息,帮助用户理解和操作。
    可定制性:提供一定的界面定制选项,例如主题切换、工具栏定制等,满足用户的个性化需求。

    用户体验优化是一个持续改进的过程,需要不断地收集用户反馈,分析用户需求,并根据用户反馈和需求进行改进和优化。

    通过功能测试、Bug 修复、性能优化和用户体验优化,我们可以将简易文本编辑器打磨得更加完善,使其成为一个功能完整、性能良好、用户体验优秀的 GUI 应用程序。这个实战案例也帮助读者综合运用了前面章节所学的 C++ GUI 编程知识,提升了实际应用开发能力。

    15. 高级主题与进阶

    本章介绍 C++ GUI 编程的一些高级主题和进阶内容,帮助读者进一步提升技能,例如自定义控件的深度开发、性能优化、跨平台兼容性高级技巧等。

    15.1 自定义控件深度开发

    深入讲解自定义控件的开发技巧,包括更复杂的绘制、事件处理、属性系统扩展等。

    15.1.1 复杂图形绘制与动画效果集成

    讲解如何在自定义控件中实现复杂图形绘制和集成动画效果。

    在前面的章节中,我们已经学习了如何创建自定义控件以及在 paintEvent() 函数中进行基本的 2D 图形绘制。为了创建更具吸引力和功能丰富的用户界面,我们需要掌握在自定义控件中进行复杂图形绘制和集成动画效果的技巧。

    复杂图形绘制

    要绘制复杂图形,QPainterPath 类是一个强大的工具。QPainterPath 允许我们创建和存储一系列的绘图操作,例如直线、曲线、弧线等,然后作为一个整体进行绘制。使用 QPainterPath 的优势在于:

    ▮ 可以构建复杂的几何形状,例如复杂的路径、轮廓,甚至是由多个简单形状组合而成的复杂图形。
    ▮ 可以重复使用和变换路径,例如平移、旋转、缩放等,而无需重新计算每个组成部分的绘制参数。
    ▮ 可以方便地进行填充、描边和裁剪操作。

    例如,我们可以使用 QPainterPath 绘制一个平滑的曲线图:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QPainter>
    3 #include <QPainterPath>
    4 #include <QVector>
    5
    6 class CurveWidget : public QWidget {
    7 public:
    8 CurveWidget(QWidget *parent = nullptr) : QWidget(parent) {
    9 // 示例数据点
    10 dataPoints = {{50, 100}, {150, 50}, {250, 150}, {350, 75}, {450, 125}};
    11 }
    12
    13 protected:
    14 void paintEvent(QPaintEvent *event) override {
    15 QPainter painter(this);
    16 painter.setRenderHint(QPainter::Antialiasing); // 开启抗锯齿
    17
    18 QPainterPath path;
    19 if (!dataPoints.isEmpty()) {
    20 path.moveTo(dataPoints.first()); // 移动到第一个点
    21 for (int i = 1; i < dataPoints.size(); ++i) {
    22 path.lineTo(dataPoints[i]); // 连接到下一个点
    23 }
    24 }
    25
    26 // 设置画笔
    27 QPen pen(Qt::blue);
    28 pen.setWidth(2);
    29 painter.setPen(pen);
    30
    31 // 绘制路径
    32 painter.drawPath(path);
    33
    34 // 绘制数据点 (可选)
    35 painter.setPen(Qt::black);
    36 painter.setBrush(Qt::white);
    37 for (const auto& point : dataPoints) {
    38 painter.drawEllipse(point, 5, 5);
    39 }
    40 }
    41
    42 private:
    43 QVector<QPointF> dataPoints;
    44 };

    在这个例子中,我们创建了一个 CurveWidget 自定义控件,使用 QPainterPath 将一系列数据点连接成平滑的曲线。moveTo() 函数用于设置路径的起始点,lineTo() 函数用于从当前点绘制直线到指定的点。

    动画效果集成

    为了给自定义控件添加动画效果,我们需要使用 Qt 的动画框架。QPropertyAnimation 是最常用的动画类,它可以对任何 QObject 对象的属性进行动画。要集成动画效果,通常需要以下步骤:

    定义需要动画的属性:在自定义控件中,确定哪些属性需要进行动画,例如位置、大小、颜色、透明度等。
    创建 QPropertyAnimation 对象:为每个需要动画的属性创建一个 QPropertyAnimation 对象。
    设置动画属性:设置动画的目标对象、动画属性、起始值、结束值和动画持续时间。
    启动动画:调用 QPropertyAnimation::start() 函数启动动画。
    连接信号与槽 (signals and slots) (可选):可以连接 QPropertyAnimation 的信号,例如 finished() 信号,在动画结束时执行某些操作。

    例如,我们可以给上面的 CurveWidget 添加一个动画效果,使其曲线可以动态绘制出来:

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QWidget>
    2 #include <QPainter>
    3 #include <QPainterPath>
    4 #include <QVector>
    5 #include <QPropertyAnimation>
    6 #include <QTimer>
    7
    8 class AnimatedCurveWidget : public QWidget {
    9 Q_OBJECT
    10 public:
    11 AnimatedCurveWidget(QWidget *parent = nullptr) : QWidget(parent), drawLength(0) {
    12 // 示例数据点
    13 dataPoints = {{50, 100}, {150, 50}, {250, 150}, {350, 75}, {450, 125}};
    14
    15 animation = new QPropertyAnimation(this, "drawLength"); // 动画属性为 drawLength
    16 animation->setDuration(1000); // 动画持续时间 1 秒
    17 animation->setStartValue(0);
    18 animation->setEndValue(1.0); // 1.0 表示绘制完整曲线
    19
    20 connect(animation, &QPropertyAnimation::valueChanged, this, [this](const QVariant &value){
    21 update(); // 动画值改变时,触发重绘
    22 });
    23
    24 animation->start(); // 启动动画
    25 }
    26
    27 qreal getDrawLength() const { return drawLength; }
    28 void setDrawLength(qreal length) { drawLength = length; }
    29
    30 protected:
    31 void paintEvent(QPaintEvent *event) override {
    32 QPainter painter(this);
    33 painter.setRenderHint(QPainter::Antialiasing);
    34
    35 QPainterPath fullPath;
    36 if (!dataPoints.isEmpty()) {
    37 fullPath.moveTo(dataPoints.first());
    38 for (int i = 1; i < dataPoints.size(); ++i) {
    39 fullPath.lineTo(dataPoints[i]);
    40 }
    41 }
    42
    43 // 获取路径的总长度
    44 qreal pathLength = fullPath.length();
    45 // 计算需要绘制的长度
    46 qreal currentLength = pathLength * drawLength;
    47
    48 QPainterPath drawnPath;
    49 if (currentLength > 0) {
    50 drawnPath = fullPath.subPath(0, currentLength); // 截取指定长度的子路径
    51 }
    52
    53
    54 QPen pen(Qt::blue);
    55 pen.setWidth(2);
    56 painter.setPen(pen);
    57
    58 painter.drawPath(drawnPath); // 绘制部分路径
    59
    60 painter.setPen(Qt::black);
    61 painter.setBrush(Qt::white);
    62 for (const auto& point : dataPoints) {
    63 painter.drawEllipse(point, 5, 5);
    64 }
    65 }
    66
    67 private:
    68 QVector<QPointF> dataPoints;
    69 QPropertyAnimation *animation;
    70 qreal drawLength; // 用于动画的属性,范围 0.0 - 1.0
    71 };

    在这个动画示例中,我们添加了一个 drawLength 属性,范围从 0.0 到 1.0,表示曲线绘制的进度。QPropertyAnimation 对象会驱动 drawLength 属性从 0.0 变化到 1.0。在 paintEvent() 函数中,我们根据 drawLength 的值,使用 QPainterPath::subPath() 函数截取路径的一部分进行绘制,从而实现了曲线的动态绘制效果。

    通过结合 QPainterPath 进行复杂图形绘制和 QPropertyAnimation 进行动画效果集成,我们可以创建出更加生动和吸引人的自定义控件。

    15.1.2 自定义事件处理与手势识别

    介绍如何处理自定义事件,以及在自定义控件中实现手势识别。

    除了响应 Qt 预定义的事件,例如鼠标事件、键盘事件等,自定义控件有时还需要处理自定义的事件。此外,为了提升用户交互体验,在自定义控件中实现手势识别也变得越来越重要。

    自定义事件处理

    自定义事件允许我们在应用程序的不同组件之间传递特定的信息或触发特定的行为。在 Qt 中,创建和处理自定义事件通常涉及以下步骤:

    定义自定义事件类型:使用 QEvent::Type 枚举定义一个新的事件类型,通常从 QEvent::User 或更高的值开始,以避免与 Qt 内部事件类型冲突。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 enum class CustomEventType : QEvent::Type {
    2 MyCustomEvent = QEvent::User + 1
    3 };

    创建自定义事件类:继承 QEvent 类,创建自定义事件类,并在其中存储事件相关的数据。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyCustomEvent : public QEvent {
    2 public:
    3 MyCustomEvent(const QString &message) : QEvent(QEvent::Type(CustomEventType::MyCustomEvent)), message(message) {}
    4 QString message;
    5 };

    发送自定义事件:使用 QCoreApplication::postEvent()QCoreApplication::sendEvent() 函数发送自定义事件。postEvent() 将事件放入事件队列,异步处理;sendEvent() 同步处理事件。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 发送自定义事件
    2 MyCustomEvent *event = new MyCustomEvent("Hello from custom event!");
    3 QCoreApplication::postEvent(receiverWidget, event); // receiverWidget 是接收事件的控件

    接收和处理自定义事件:在接收事件的自定义控件中,重写 event() 函数,并在其中处理自定义事件。需要判断事件类型是否为自定义事件类型,如果是,则将 QEvent 对象强制转换为自定义事件类,并提取事件数据进行处理。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 bool MyWidget::event(QEvent *event) override {
    2 if (event->type() == QEvent::Type(CustomEventType::MyCustomEvent)) {
    3 MyCustomEvent *customEvent = static_cast<MyCustomEvent*>(event);
    4 qDebug() << "Custom Event Received:" << customEvent->message;
    5 return true; // 标记事件已被处理
    6 }
    7 return QWidget::event(event); // 调用父类的事件处理函数,处理其他事件
    8 }

    通过自定义事件,我们可以实现组件间的解耦和灵活的消息传递机制。

    手势识别

    Qt 提供了强大的手势识别框架,可以方便地在自定义控件中实现各种手势操作,例如拖拽 (Drag)、滑动 (Swipe)、捏合缩放 (Pinch Zoom)、旋转 (Rotate) 等。实现手势识别通常需要以下步骤:

    启用手势识别:在自定义控件的构造函数中,使用 grabGesture() 函数启用需要识别的手势类型。Qt 预定义了多种手势类型,例如 Qt::PanGesture (拖拽手势)、Qt::SwipeGesture (滑动/轻扫手势)、Qt::PinchGesture (捏合手势) 等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyGestureWidget : public QWidget {
    2 public:
    3 MyGestureWidget(QWidget *parent = nullptr) : QWidget(parent) {
    4 grabGesture(Qt::PanGesture); // 启用拖拽手势
    5 grabGesture(Qt::PinchGesture); // 启用捏合手势
    6 }
    7
    8 protected:
    9 bool event(QEvent *event) override {
    10 if (event->type() == QEvent::Gesture) {
    11 return gestureEvent(static_cast<QGestureEvent*>(event)); // 处理手势事件
    12 }
    13 return QWidget::event(event);
    14 }
    15
    16 bool gestureEvent(QGestureEvent *event) {
    17 if (QGesture *pan = event->gesture(Qt::PanGesture)) {
    18 handlePanGesture(static_cast<QPanGesture*>(pan)); // 处理拖拽手势
    19 }
    20 if (QGesture *pinch = event->gesture(Qt::PinchGesture)) {
    21 handlePinchGesture(static_cast<QPinchGesture*>(pinch)); // 处理捏合手势
    22 }
    23 return true; // 标记手势事件已被处理
    24 }
    25
    26 private slots:
    27 void handlePanGesture(QPanGesture *gesture) {
    28 if (gesture->state() == Qt::GestureUpdated) {
    29 QPointF delta = gesture->delta(); // 获取拖拽的偏移量
    30 // 根据偏移量更新控件的位置或内容
    31 qDebug() << "Pan Gesture Updated, Delta:" << delta;
    32 }
    33 }
    34
    35 void handlePinchGesture(QPinchGesture *gesture) {
    36 if (gesture->state() == Qt::GestureUpdated) {
    37 qreal scaleFactor = gesture->scaleFactor(); // 获取缩放因子
    38 qreal rotationAngle = gesture->rotationAngle(); // 获取旋转角度
    39 // 根据缩放因子和旋转角度更新控件的缩放和旋转
    40 qDebug() << "Pinch Gesture Updated, Scale Factor:" << scaleFactor << ", Rotation Angle:" << rotationAngle;
    41 }
    42 }
    43 };

    处理手势事件:重写 event() 函数,并在其中判断事件类型是否为 QEvent::Gesture,如果是,则调用 gestureEvent() 函数进行手势事件处理。
    处理具体手势:在 gestureEvent() 函数中,使用 QGestureEvent::gesture() 函数获取具体的手势对象 (例如 QPanGesture, QPinchGesture 等),然后根据手势的状态 (例如 Qt::GestureStarted, Qt::GestureUpdated, Qt::GestureFinished, Qt::GestureCanceled) 和手势数据 (例如拖拽偏移量、缩放因子、旋转角度等) 进行相应的处理。

    通过 Qt 的手势识别框架,我们可以方便地为自定义控件添加丰富的交互操作,提升用户体验,尤其是在触摸屏设备上。

    15.1.3 扩展属性系统:自定义属性编辑器 (Property Editor)

    讲解如何扩展属性系统,为自定义控件创建自定义属性编辑器 (Property Editor)。

    Qt 的属性系统 (Property System) 是其元对象系统 (Meta-Object System) 的重要组成部分,它允许我们在运行时动态地访问和修改对象的属性。为了更好地管理和编辑自定义控件的属性,我们可以扩展属性系统,并创建自定义的属性编辑器 (Property Editor)。

    扩展属性系统

    扩展属性系统主要涉及以下几个方面:

    声明属性:在自定义控件的类定义中,使用 Q_PROPERTY() 宏声明属性。Q_PROPERTY() 宏需要指定属性的类型、属性名、读函数 (READ)、写函数 (WRITE) 和可选的信号 (NOTIFY) 等信息。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyCustomControl : public QWidget {
    2 Q_OBJECT
    3 Q_PROPERTY(QString text READ text WRITE setText NOTIFY textChanged) // 声明 text 属性
    4 public:
    5 MyCustomControl(QWidget *parent = nullptr) : QWidget(parent) {}
    6
    7 QString text() const { return _text; }
    8 void setText(const QString &text) {
    9 if (_text != text) {
    10 _text = text;
    11 emit textChanged(_text); // 发射属性改变信号
    12 update(); // 触发重绘
    13 }
    14 }
    15
    16 signals:
    17 void textChanged(const QString &text); // 属性改变信号
    18
    19 private:
    20 QString _text;
    21 };

    在这个例子中,我们使用 Q_PROPERTY() 宏声明了一个名为 text 的属性,类型为 QString,读函数为 text(),写函数为 setText(),属性改变信号为 textChanged()

    实现读写函数:为声明的属性实现读函数 (READ) 和写函数 (WRITE)。读函数负责返回属性的值,写函数负责设置属性的值,并在属性值改变时发射属性改变信号 (如果声明了 NOTIFY 信号)。
    发射属性改变信号:在属性值改变时,务必发射通过 Q_PROPERTY() 宏声明的属性改变信号 (NOTIFY)。这样,当属性值发生变化时,连接到该信号的槽函数就会被调用,从而可以响应属性的变化。

    通过 Q_PROPERTY() 宏和相应的读写函数,我们将 C++ 类的成员变量暴露为 Qt 的属性,从而可以使用 Qt 的属性系统进行访问和操作。

    自定义属性编辑器 (Property Editor)

    为了更方便地编辑自定义控件的属性,我们可以创建自定义的属性编辑器 (Property Editor)。Qt 提供了 Qt Property Browser 库,可以帮助我们快速创建属性编辑器。使用 Qt Property Browser 库创建自定义属性编辑器通常涉及以下步骤:

    安装 Qt Property Browser:确保你的 Qt 安装包含了 Qt Property Browser 库。如果未安装,需要通过 Qt Maintenance Tool 安装。
    在项目中添加 Qt Property Browser:在项目的 .pro 文件中添加 QT += propertybrowser
    创建属性管理器 (Property Manager)Qt Property Browser 库使用属性管理器 (Property Manager) 来管理属性。对于基本类型属性,可以使用 QtVariantPropertyManager;对于枚举类型属性,可以使用 QtEnumPropertyManager;对于标志类型属性,可以使用 QtFlagPropertyManager;对于颜色属性,可以使用 QtColorPropertyManager 等。如果需要自定义属性类型,可以继承 QtAbstractPropertyManager 创建自定义属性管理器。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <qtpropertybrowser/qtpropertybrowser.h>
    2 #include <qtpropertybrowser/qtvariantpropertymanager.h>
    3 #include <qtpropertybrowser/qtpropertybrowserwidget.h>
    4
    5 class MyPropertyEditorWidget : public QWidget {
    6 public:
    7 MyPropertyEditorWidget(QWidget *parent = nullptr) : QWidget(parent) {
    8 QtVariantPropertyManager *variantManager = new QtVariantPropertyManager(this); // 创建属性管理器
    9 QtPropertySet *propertySet = variantManager->createPropertySet(); // 创建属性集合
    10
    11 // 添加属性到属性集合
    12 QtProperty *textProperty = variantManager->addProperty(QVariant::String, "Text");
    13 propertySet->addProperty(textProperty);
    14
    15 QtPropertyBrowserWidget *propertyBrowser = new QtPropertyBrowserWidget(this); // 创建属性浏览器控件
    16 propertyBrowser->setFactoryForManager(variantManager, new QtVariantEditorFactory(this)); // 设置编辑器工厂
    17 propertyBrowser->addPropertySet(propertySet); // 添加属性集合到属性浏览器
    18
    19 QVBoxLayout *layout = new QVBoxLayout(this);
    20 layout->addWidget(propertyBrowser);
    21 }
    22 };

    创建属性集合 (Property Set):使用属性管理器的 createPropertySet() 函数创建一个属性集合 (Property Set)。属性集合用于组织和分组属性。
    添加属性到属性集合:使用属性管理器的 addProperty() 函数创建属性,并使用属性集合的 addProperty() 函数将属性添加到属性集合中。addProperty() 函数需要指定属性的类型和属性名。
    创建属性浏览器控件 (Property Browser Widget):创建 QtPropertyBrowserWidget 控件,用于显示和编辑属性。
    设置编辑器工厂 (Editor Factory):使用属性浏览器控件的 setFactoryForManager() 函数为属性管理器设置编辑器工厂 (Editor Factory)。编辑器工厂负责为不同类型的属性创建编辑器。Qt 提供了默认的编辑器工厂,例如 QtVariantEditorFactory 用于基本类型属性。如果需要自定义属性编辑器,可以继承 QtAbstractEditorFactory 创建自定义编辑器工厂。
    将属性集合添加到属性浏览器:使用属性浏览器控件的 addPropertySet() 函数将属性集合添加到属性浏览器中。

    通过 Qt Property Browser 库,我们可以快速创建功能强大的属性编辑器,方便用户在界面上直接编辑自定义控件的属性值。对于更高级的属性编辑需求,例如自定义属性类型、自定义编辑器外观等,还可以深入研究 Qt Property Browser 库的文档,进行更高级的定制。

    15.2 GUI 程序性能优化

    深入探讨 GUI 程序的性能优化技巧,包括渲染优化、内存优化、多线程优化等方面。

    GUI 程序的性能直接影响用户体验。一个响应迅速、运行流畅的 GUI 程序能够显著提升用户满意度。本节将深入探讨 GUI 程序的性能优化技巧,从渲染、内存和多线程等方面入手,帮助读者构建高性能的 GUI 应用程序。

    15.2.1 渲染优化:硬件加速、减少绘制操作、缓存机制

    讲解渲染优化技巧,如利用硬件加速、减少绘制操作、使用缓存机制。

    GUI 程序的渲染性能是影响用户体验的关键因素之一。渲染优化旨在提高图形绘制的效率,减少 CPU 和 GPU 的负载,从而实现更流畅的界面动画和更快的响应速度。以下是一些常用的渲染优化技巧:

    硬件加速 (Hardware Acceleration)

    硬件加速是指利用 GPU (图形处理器) 进行图形渲染,以减轻 CPU 的负担,并提高渲染性能。Qt 框架默认情况下会尽可能地利用硬件加速。为了确保硬件加速有效,需要注意以下几点:

    检查硬件加速是否启用:可以使用 QSurfaceFormat::setRenderableType()QSurfaceFormat::setProfile() 等函数配置 OpenGL 表面格式,并确保选择了支持硬件加速的渲染类型和 Profile。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QSurfaceFormat format;
    2 format.setRenderableType(QSurfaceFormat::OpenGL); // 使用 OpenGL 渲染
    3 format.setProfile(QSurfaceFormat::CoreProfile); // 使用 Core Profile
    4 format.setVersion(3, 3); // 设置 OpenGL 版本
    5 QSurfaceFormat::setDefaultFormat(format); // 设置为默认格式

    使用支持硬件加速的 API:Qt Quick (QML) 场景是为硬件加速设计的,因此在需要高性能渲染的场景下,优先考虑使用 Qt Quick。对于 QtWidgets 应用程序,可以使用 QOpenGLWidgetQGraphicsView 结合 OpenGL 进行硬件加速渲染。
    避免软件渲染:软件渲染完全依赖 CPU 进行图形计算,性能较低。应尽量避免使用软件渲染,例如避免使用 QImage 作为绘图设备进行实时渲染,因为 QImage 通常是软件渲染的。

    减少绘制操作

    减少不必要的绘制操作是提高渲染性能的有效方法。以下是一些减少绘制操作的技巧:

    按需绘制 (On-Demand Painting):只在需要更新界面时才进行重绘。例如,只在数据变化、窗口大小改变或控件属性改变时调用 update() 函数触发重绘。避免不必要的定时器触发重绘或在循环中频繁重绘。
    裁剪区域 (Clip Region):使用 QPainter::setClipRegion()QPainter::setClipRect() 函数设置裁剪区域,限制绘制操作只在需要更新的区域进行。这样可以避免重绘整个控件或窗口,提高绘制效率。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void MyWidget::paintEvent(QPaintEvent *event) override {
    2 QPainter painter(this);
    3 painter.setClipRegion(event->region()); // 设置裁剪区域为事件的更新区域
    4 // ... 绘制代码 ...
    5 }

    脏区域 (Dirty Region) 管理:在自定义控件中,可以维护一个脏区域 (Dirty Region),记录需要重绘的区域。每次只重绘脏区域,而不是整个控件。Qt 的 QWidgetQGraphicsView 框架内部都使用了脏区域管理机制。
    避免复杂的绘制操作:尽量避免在 paintEvent() 函数中进行复杂的计算或耗时的操作,例如文件 I/O、网络请求等。这些操作应放在后台线程中进行,避免阻塞 GUI 线程,影响界面响应速度。
    减少图层 (Layer) 数量:在 Qt Quick 场景中,过多的图层会增加 GPU 的负担。应尽量减少图层数量,合并可以合并的图层,优化图层结构。

    缓存机制 (Caching Mechanisms)

    缓存机制是指将一些计算结果或绘制结果缓存起来,以便在下次需要时直接使用,避免重复计算或绘制,提高性能。以下是一些常用的缓存机制:

    图像缓存 (Image Caching):对于静态的或不经常变化的图形元素,例如背景图片、图标等,可以预先将其绘制到 QImageQPixmap 中,然后直接绘制缓存的图像,而不是每次都重新绘制。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class MyCachedWidget : public QWidget {
    2 public:
    3 MyCachedWidget(QWidget *parent = nullptr) : QWidget(parent) {
    4 // 预先绘制背景图像并缓存
    5 backgroundImage = QImage(size(), QImage::Format_ARGB32_Premultiplied);
    6 QPainter painter(&backgroundImage);
    7 painter.fillRect(rect(), Qt::lightGray);
    8 // ... 绘制背景元素 ...
    9 }
    10
    11 protected:
    12 void paintEvent(QPaintEvent *event) override {
    13 QPainter painter(this);
    14 painter.drawImage(rect(), backgroundImage); // 绘制缓存的背景图像
    15 // ... 绘制动态元素 ...
    16 }
    17
    18 private:
    19 QImage backgroundImage; // 缓存的背景图像
    20 };

    绘制路径缓存 (Painter Path Caching):对于复杂的几何形状,可以使用 QPainterPath 将其绘制路径缓存起来,然后直接绘制缓存的路径,而不是每次都重新计算路径。
    OpenGL 缓存对象 (OpenGL Buffer Objects):在使用 OpenGL 进行渲染时,可以使用 VBO (顶点缓冲区对象, Vertex Buffer Object) 和 IBO (索引缓冲区对象, Index Buffer Object) 等 OpenGL 缓存对象,将顶点数据和索引数据缓存到 GPU 显存中,减少数据传输和顶点计算的开销。
    数据缓存 (Data Caching):对于需要频繁访问的数据,可以使用缓存 (Cache) 将其存储在内存中,提高数据访问速度。可以使用 Qt 提供的 QCacheQHash 等容器实现数据缓存。

    通过合理地利用硬件加速、减少绘制操作和使用缓存机制,可以显著提高 GUI 程序的渲染性能,实现更流畅的界面效果。在实际开发中,需要根据具体的应用场景和性能瓶颈,选择合适的优化策略。

    15.2.2 内存优化:对象池、资源管理、避免内存泄漏

    介绍内存优化技巧,如使用对象池、资源管理、避免内存泄漏。

    内存管理是 GUI 程序性能优化的重要方面。合理的内存管理可以减少内存占用,提高程序的运行效率和稳定性。内存优化主要包括以下几个方面:

    对象池 (Object Pooling)

    对象池是一种创建和管理一组可重用对象的技术。当需要使用对象时,从对象池中获取,使用完毕后,将对象返回到对象池,而不是销毁。对象池可以减少对象的创建和销毁开销,提高性能,并减少内存碎片。对象池适用于以下场景:

    频繁创建和销毁的对象:例如,在动画或游戏场景中,需要频繁创建和销毁大量的图形对象。
    创建开销较大的对象:例如,创建数据库连接或网络连接的对象。

    实现对象池的基本思路如下:

    创建对象池类:创建一个对象池类,用于管理一组对象。对象池类需要提供获取对象和释放对象的接口。
    预先创建对象:在对象池初始化时,预先创建一定数量的对象,并将其存储在对象池中。
    获取对象:当需要使用对象时,从对象池中获取一个空闲对象。如果对象池中没有空闲对象,可以创建新的对象,或者等待对象池中有对象释放。
    释放对象:当对象使用完毕后,将其返回到对象池中,标记为空闲状态,以便下次重用。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QObject>
    2 #include <QQueue>
    3 #include <QMutex>
    4
    5 template <typename T>
    6 class ObjectPool {
    7 public:
    8 ObjectPool(int initialSize = 10) {
    9 for (int i = 0; i < initialSize; ++i) {
    10 availableObjects.enqueue(new T()); // 预先创建对象
    11 }
    12 }
    13
    14 ~ObjectPool() {
    15 QMutexLocker locker(&mutex);
    16 while (!availableObjects.isEmpty()) {
    17 delete availableObjects.dequeue(); // 释放所有对象
    18 }
    19 while (!inUseObjects.isEmpty()) {
    20 delete inUseObjects.dequeue(); // 释放正在使用的对象 (可选,取决于应用场景)
    21 }
    22 }
    23
    24 T* acquireObject() {
    25 QMutexLocker locker(&mutex);
    26 if (availableObjects.isEmpty()) {
    27 return new T(); // 如果对象池为空,创建新对象 (或者可以等待或抛出异常)
    28 }
    29 T* object = availableObjects.dequeue();
    30 inUseObjects.enqueue(object);
    31 return object;
    32 }
    33
    34 void releaseObject(T* object) {
    35 QMutexLocker locker(&mutex);
    36 if (inUseObjects.contains(object)) {
    37 inUseObjects.removeOne(object);
    38 availableObjects.enqueue(object); // 将对象返回对象池
    39 }
    40 }
    41
    42 private:
    43 QQueue<T*> availableObjects; // 空闲对象队列
    44 QQueue<T*> inUseObjects; // 正在使用的对象队列
    45 QMutex mutex; // 互斥锁,保护对象池的线程安全
    46 };

    这是一个简单的对象池模板类的示例。在实际应用中,需要根据具体的对象类型和使用场景,进行适当的调整和扩展。例如,可以添加对象池的最大容量限制、对象创建策略、对象清理策略等。

    资源管理 (Resource Management)

    资源管理是指有效地管理程序中使用的各种资源,例如内存、文件句柄、网络连接、OpenGL 资源等。良好的资源管理可以避免资源泄漏,提高程序的稳定性和性能。以下是一些资源管理的技巧:

    RAII (Resource Acquisition Is Initialization) 资源获取即初始化:RAII 是一种 C++ 编程技术,它将资源的生命周期与对象的生命周期绑定在一起。在对象构造时获取资源,在对象析构时释放资源。通过 RAII,可以确保资源在任何情况下都能被正确释放,即使发生异常。Qt 的许多类都使用了 RAII 技术,例如 QFile, QTcpSocket, QMutexLocker 等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void processFile(const QString &filePath) {
    2 QFile file(filePath); // RAII: 对象构造时获取文件资源
    3 if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
    4 QTextStream in(&file);
    5 QString line = in.readLine();
    6 // ... 处理文件内容 ...
    7 } // RAII: 对象析构时释放文件资源 (无论是否发生异常)
    8 // 文件资源已自动释放
    9 }

    智能指针 (Smart Pointers):智能指针是一种 C++ 编程技术,用于自动管理动态分配的内存。Qt 提供了 QSharedPointerQScopedPointer 等智能指针类。QSharedPointer 允许多个智能指针共享同一个对象的所有权,当最后一个 QSharedPointer 销毁时,对象才会被删除。QScopedPointer 限制只有一个智能指针拥有对象的所有权,当 QScopedPointer 销毁时,对象会被删除。使用智能指针可以避免手动管理内存,减少内存泄漏和悬挂指针的风险。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 void processData() {
    2 QSharedPointer<MyObject> obj(new MyObject()); // 使用 QSharedPointer 管理对象内存
    3 // ... 使用 obj ...
    4 // 对象内存会在 QSharedPointer 销毁时自动释放
    5 }

    及时释放不再使用的资源:对于不再使用的资源,例如动态分配的内存、打开的文件句柄、网络连接等,应及时释放,避免资源泄漏。可以使用 delete 运算符释放动态分配的内存,使用 QFile::close() 关闭文件句柄,使用 QTcpSocket::disconnectFromHost() 断开网络连接等。
    限制资源使用量:对于一些资源消耗较大的操作,例如图像加载、网络请求等,可以限制其并发数量或使用量,避免资源耗尽,导致程序崩溃或性能下降。可以使用线程池、连接池等技术限制资源使用量。

    避免内存泄漏 (Memory Leaks)

    内存泄漏是指程序在动态分配内存后,未能及时释放,导致内存占用不断增加,最终耗尽系统内存,导致程序崩溃或系统性能下降。GUI 程序由于其生命周期较长,更容易发生内存泄漏。以下是一些避免内存泄漏的技巧:

    使用 RAII 和智能指针:如前所述,使用 RAII 和智能指针可以自动管理资源,减少内存泄漏的风险。
    检查 newdelete 的配对使用:对于手动分配的内存,务必确保 newdelete 配对使用,每次使用 new 分配内存后,都要在适当的时候使用 delete 释放内存。
    避免循环引用:在 Qt 对象树 (Object Tree) 中,父子对象之间存在所有权关系。当父对象销毁时,会自动删除所有子对象。但如果存在循环引用,例如 A 对象是 B 对象的父对象,同时 B 对象又是 A 对象的子对象,就会导致对象无法正常销毁,造成内存泄漏。应避免创建循环引用,或者使用弱引用 (Weak Reference) 打破循环引用。
    使用内存泄漏检测工具:可以使用内存泄漏检测工具,例如 Valgrind (Linux 平台) 或 AddressSanitizer (跨平台),检测程序是否存在内存泄漏。这些工具可以帮助我们定位内存泄漏的位置,并及时修复。

    通过合理地使用对象池、资源管理和避免内存泄漏,可以有效地优化 GUI 程序的内存使用,提高程序的稳定性和性能。内存优化是一个持续的过程,需要在程序开发的不同阶段,不断地进行监控和调整。

    15.2.3 多线程优化:任务分解、线程池、异步操作

    讲解多线程优化技巧,如合理分解任务、使用线程池、采用异步操作。

    多线程技术可以充分利用多核处理器的性能,提高 GUI 程序的并发性和响应速度。对于 GUI 程序来说,多线程优化尤其重要,因为 GUI 线程 (主线程) 负责处理用户界面事件和绘制操作,如果 GUI 线程被耗时操作阻塞,就会导致界面卡顿,影响用户体验。多线程优化的主要目标是将耗时操作放到后台线程中执行,避免阻塞 GUI 线程。以下是一些常用的多线程优化技巧:

    任务分解 (Task Decomposition)

    任务分解是指将一个大的耗时任务分解成多个小的子任务,然后将这些子任务分配到不同的线程中并行执行。任务分解可以提高程序的并发性,缩短总的执行时间。任务分解的关键在于:

    识别耗时任务:首先需要识别程序中的耗时任务,例如文件 I/O、网络请求、复杂的计算、图像处理等。这些任务通常是多线程优化的重点。
    分解任务:将耗时任务分解成多个独立的子任务。子任务之间应尽可能地减少依赖关系,以便并行执行。任务分解的粒度要适中,过小的子任务会增加线程调度的开销,过大的子任务则无法充分利用多线程的优势。
    分配任务:将分解后的子任务分配到不同的线程中执行。可以使用线程池或手动创建线程的方式分配任务。

    例如,对于一个需要加载大量图像的任务,可以将其分解成多个子任务,每个子任务负责加载一部分图像。然后将这些子任务分配到线程池中并行执行,从而加快图像加载速度。

    线程池 (Thread Pool)

    线程池是一种管理和重用线程的技术。线程池预先创建一组线程,并将其放入线程池中。当需要执行任务时,从线程池中获取一个空闲线程来执行任务,任务执行完毕后,线程返回到线程池中,等待执行下一个任务。线程池可以减少线程的创建和销毁开销,提高线程的重用率,并限制并发线程的数量,避免系统资源耗尽。Qt 提供了 QThreadPool 类来实现线程池。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QThreadPool>
    2 #include <QRunnable>
    3 #include <QDebug>
    4
    5 class MyTask : public QRunnable {
    6 public:
    7 MyTask(int taskId) : taskId(taskId) {}
    8
    9 void run() override {
    10 qDebug() << "Task" << taskId << "started in thread:" << QThread::currentThreadId();
    11 QThread::sleep(2); // 模拟耗时操作
    12 qDebug() << "Task" << taskId << "finished in thread:" << QThread::currentThreadId();
    13 }
    14
    15 private:
    16 int taskId;
    17 };
    18
    19 void processTasks() {
    20 QThreadPool *threadPool = QThreadPool::globalInstance(); // 获取全局线程池实例
    21 threadPool->setMaxThreadCount(4); // 设置最大线程数
    22
    23 for (int i = 0; i < 10; ++i) {
    24 MyTask *task = new MyTask(i);
    25 threadPool->start(task); // 将任务添加到线程池
    26 }
    27
    28 threadPool->waitForDone(); // 等待所有任务完成
    29 qDebug() << "All tasks finished.";
    30 }

    在这个例子中,我们使用了 QThreadPool::globalInstance() 获取全局线程池实例,并设置了最大线程数为 4。然后创建了 10 个 MyTask 任务,并将它们添加到线程池中并行执行。QThreadPool::waitForDone() 函数用于等待所有任务完成。

    异步操作 (Asynchronous Operations)

    异步操作是指在执行耗时操作时,不会阻塞当前线程,而是立即返回,并在操作完成后通过回调函数或信号通知结果。异步操作可以提高程序的响应速度,避免界面卡顿。Qt 提供了多种异步操作机制:

    信号与槽 (Signals and Slots) 的异步连接:Qt 的信号与槽机制支持异步连接。当信号发射时,如果连接类型为 Qt::QueuedConnectionQt::AutoQueuedConnection,槽函数将在接收对象的事件循环中异步执行。可以使用异步信号与槽来实现线程间的通信和异步任务的处理。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 假设 MyWorkerThread 是一个工作线程类,它发射信号 taskFinished(ResultType result)
    2 void MyWidget::startTask() {
    3 MyWorkerThread *workerThread = new MyWorkerThread();
    4 connect(workerThread, &MyWorkerThread::taskFinished, this, &MyWidget::handleTaskResult, Qt::QueuedConnection); // 异步连接信号与槽
    5 workerThread->start(); // 启动工作线程
    6 }
    7
    8 void MyWidget::handleTaskResult(ResultType result) {
    9 // 在 GUI 线程中处理任务结果
    10 qDebug() << "Task result received in GUI thread.";
    11 }

    QtConcurrent 框架QtConcurrent 框架提供了一组用于并发编程的 API,包括 QtConcurrent::run(), QtConcurrent::map(), QtConcurrent::filter(), QtConcurrent::reduce() 等函数。QtConcurrent::run() 函数可以在线程池中异步运行一个函数,并返回 QFuture 对象,用于获取异步操作的结果。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QtConcurrent>
    2
    3 QString processData(const QString &data) {
    4 // 耗时的数据处理操作
    5 QThread::sleep(3);
    6 return data.toUpper();
    7 }
    8
    9 void MyWidget::startAsyncProcess() {
    10 QFuture<QString> future = QtConcurrent::run(processData, "hello world"); // 异步运行 processData 函数
    11 // ... 可以继续执行 GUI 线程的其他操作 ...
    12
    13 // 在需要结果时,可以使用 future.result() 获取结果 (会阻塞当前线程,直到结果可用)
    14 // 或者使用 future.then() 注册一个在结果可用时异步执行的回调函数
    15 future.then([this](const QString &result){
    16 // 在线程池线程中执行回调函数 (默认)
    17 qDebug() << "Async process result:" << result << "in thread:" << QThread::currentThreadId();
    18 // 如果需要在 GUI 线程中更新界面,需要使用信号与槽或 QMetaObject::invokeMethod() 切换到 GUI 线程
    19 QMetaObject::invokeMethod(this, "updateUI", Qt::QueuedConnection, Q_ARG(QString, result));
    20 });
    21 }
    22
    23 void MyWidget::updateUI(const QString &result) {
    24 // 在 GUI 线程中更新界面
    25 qDebug() << "Update UI with result:" << result << "in GUI thread.";
    26 }

    QFutureQPromiseQFutureQPromise 是 C++11 标准库中的 std::futurestd::promise 的 Qt 版本,用于实现异步编程。QPromise 对象用于设置异步操作的结果,QFuture 对象用于获取异步操作的结果。

    通过合理地分解任务、使用线程池和采用异步操作,可以将耗时操作放到后台线程中执行,避免阻塞 GUI 线程,提高 GUI 程序的响应速度和用户体验。多线程编程需要注意线程安全和线程同步问题,避免数据竞争和死锁等并发错误。Qt 提供了丰富的线程同步机制,例如互斥锁 (Mutex)、读写锁 (Read-Write Lock)、条件变量 (Condition Variable)、原子操作 (Atomic Operations) 等,可以用于保护共享数据,实现线程间的同步。

    15.3 跨平台兼容性高级技巧

    介绍跨平台 GUI 开发的高级技巧,解决不同平台间的差异性问题,提高应用程序的跨平台兼容性。

    跨平台是 Qt 框架的核心优势之一。使用 Qt 开发的 GUI 应用程序可以很容易地在 Windows、macOS、Linux 等多个平台上编译和运行。然而,不同的平台之间仍然存在一些差异性,例如操作系统 API、界面风格、文件系统等。为了实现更好的跨平台兼容性,需要掌握一些高级技巧,以解决这些平台差异性问题。

    15.3.1 平台差异性分析:操作系统 API, 界面风格, 文件系统

    分析不同平台间的差异性,如操作系统 API, 界面风格, 文件系统等。

    在进行跨平台 GUI 开发时,需要了解不同平台之间的差异性,以便在开发过程中采取相应的措施,提高应用程序的跨平台兼容性。主要的平台差异性包括以下几个方面:

    操作系统 API (Operating System APIs)

    不同的操作系统提供了不同的 API 来访问系统资源和功能,例如窗口管理、进程管理、网络通信、文件系统访问、硬件设备访问等。虽然 Qt 框架对这些操作系统 API 进行了抽象和封装,提供了跨平台的 API,但在某些情况下,我们可能需要直接调用平台特定的 API 来实现某些高级功能或利用平台特性。平台特定的 API 差异主要体现在:

    窗口系统:Windows 使用 Win32 API 或 WinRT API 进行窗口管理,macOS 使用 Cocoa API 进行窗口管理,Linux (X11) 使用 Xlib 或 XCB API 进行窗口管理,Wayland 使用 Wayland 协议进行窗口管理。Qt 框架使用统一的 QWidgetQWindow 类来抽象不同平台的窗口系统,但底层实现仍然依赖于平台特定的 API。
    线程和进程:不同的操作系统提供了不同的线程和进程管理 API。Windows 使用 Win32 API 进行线程和进程管理,macOS 和 Linux 使用 POSIX 线程 (pthread) 和 POSIX 进程 API。Qt 框架使用统一的 QThreadQProcess 类来抽象不同平台的线程和进程 API。
    网络编程:不同的操作系统提供了不同的网络编程 API。Windows 使用 Winsock API,macOS 和 Linux 使用 BSD Socket API。Qt 框架使用统一的 QTcpSocket, QUdpSocket, QNetworkAccessManager 等类来抽象不同平台的网络编程 API。
    文件系统:不同的操作系统文件系统结构和路径分隔符不同。Windows 使用反斜杠 \ 作为路径分隔符,macOS 和 Linux 使用正斜杠 / 作为路径分隔符。Qt 框架使用 QDirQFile 类来抽象不同平台的文件系统,并提供了跨平台的路径处理 API,例如 QDir::separator(), QDir::cleanPath() 等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 跨平台路径分隔符
    2 qDebug() << "Path separator:" << QDir::separator();
    3
    4 // 跨平台文件路径拼接
    5 QString filePath = QDir::cleanPath(QDir::homePath() + QDir::separator() + "Documents" + QDir::separator() + "myfile.txt");
    6 qDebug() << "File path:" << filePath;

    界面风格 (User Interface Style)

    不同的操作系统有不同的默认界面风格和用户界面规范。Windows 默认使用 Windows 风格,macOS 默认使用 macOS 风格,Linux (GNOME) 默认使用 GTK+ 风格,Linux (KDE) 默认使用 Qt 风格。Qt 框架提供了多种界面风格 (Style),可以使用 QApplication::setStyle() 函数设置应用程序的界面风格。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 设置应用程序界面风格为 Fusion 风格 (跨平台通用风格)
    2 QApplication::setStyle("Fusion");
    3
    4 // 设置应用程序界面风格为平台原生风格
    5 QApplication::setStyle(QStyleFactory::create("native")); // "native" 代表平台原生风格

    为了实现更好的跨平台用户体验,需要考虑以下界面风格差异:

    控件外观:不同的平台控件外观 (例如按钮、菜单、滚动条等) 可能略有不同。Qt 框架会根据当前平台的默认风格,自动调整控件的外观。可以使用 Qt Style Sheets (样式表) 自定义控件的外观,统一不同平台下的界面风格。
    字体:不同的平台默认字体和字体渲染方式可能不同。可以使用 QFontDatabase 类获取系统字体信息,并设置应用程序的默认字体,以保证在不同平台上的字体显示效果一致。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 设置应用程序默认字体为 "微软雅黑" (Windows 平台常用字体)
    2 QFont font("Microsoft YaHei");
    3 QApplication::setFont(font);

    菜单栏位置:在 macOS 平台上,应用程序的菜单栏通常位于屏幕顶部,而不是在应用程序窗口内部。Qt 框架会自动处理 macOS 平台的菜单栏位置,将主窗口的菜单栏移动到屏幕顶部。在 Windows 和 Linux 平台上,菜单栏通常位于应用程序窗口内部。
    对话框风格:不同的平台对话框风格可能略有不同。Qt 框架会根据当前平台的默认风格,自动调整对话框的外观。可以使用 QMessageBox, QFileDialog, QColorDialog 等类创建跨平台的标准对话框。

    文件系统 (File System)

    不同的操作系统文件系统结构和文件路径规范不同。主要的文件系统差异主要体现在:

    路径分隔符:如前所述,Windows 使用反斜杠 \,macOS 和 Linux 使用正斜杠 /。Qt 内部会自动处理路径分隔符的转换,通常可以使用正斜杠 / 作为跨平台通用的路径分隔符。QDir::separator() 函数可以获取当前平台的文件路径分隔符。
    大小写敏感性:Windows 文件系统通常是大小写不敏感的,例如 myfile.txtMyFile.TXT 被认为是同一个文件。macOS 和 Linux 文件系统通常是大小写敏感的,myfile.txtMyFile.TXT 被认为是不同的文件。在跨平台开发时,需要注意文件名的 大小写一致性 问题,避免在大小写敏感的平台上出现文件找不到的问题。
    根目录:Windows 文件系统以盘符 (例如 C:\, D:\) 作为根目录,每个盘符代表一个独立的卷。macOS 和 Linux 文件系统只有一个根目录 /,所有文件和目录都挂载在根目录下。在跨平台开发时,应避免硬编码盘符路径,使用相对路径或 Qt 提供的标准路径 API,例如 QDir::homePath(), QDir::tempPath() 等。
    特殊目录:不同的平台有不同的特殊目录,例如用户主目录、临时目录、应用程序数据目录等。Qt 提供了 QStandardPaths 类来获取跨平台的标准目录路径。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 // 获取用户文档目录
    2 QString documentsPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
    3 qDebug() << "Documents path:" << documentsPath;
    4
    5 // 获取应用程序数据目录
    6 QString appDataPath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
    7 qDebug() << "App data path:" << appDataPath;
    8
    9 // 获取临时目录
    10 QString tempPath = QDir::tempPath();
    11 qDebug() << "Temp path:" << tempPath;

    了解这些平台差异性,可以帮助我们在跨平台 GUI 开发中,编写更具兼容性的代码,避免在特定平台上出现问题。Qt 框架已经为我们做了很多跨平台兼容性的工作,但作为开发者,我们仍然需要了解这些差异,并在必要时采取相应的措施。

    15.3.2 条件编译 (Conditional Compilation) 与平台特性检测

    讲解如何使用条件编译和平台特性检测技术,处理平台差异性。

    虽然 Qt 框架提供了强大的跨平台抽象层,但在某些情况下,我们仍然需要编写平台特定的代码,例如调用平台特定的 API 或处理平台特定的行为。为了实现平台特定的代码逻辑,可以使用条件编译 (Conditional Compilation) 和平台特性检测技术。

    条件编译 (Conditional Compilation)

    条件编译是指在编译时根据不同的条件选择性地编译不同的代码段。C++ 预处理器提供了 #ifdef, #ifndef, #else, #elif, #endif 等预处理指令,可以用于实现条件编译。Qt 框架预定义了一些宏,用于标识不同的平台和 Qt 版本,例如:

    平台宏
    ▮▮▮▮⚝ Q_OS_WINDOWS: Windows 平台
    ▮▮▮▮⚝ Q_OS_MACOS: macOS 平台
    ▮▮▮▮⚝ Q_OS_LINUX: Linux 平台
    ▮▮▮▮⚝ Q_OS_ANDROID: Android 平台
    ▮▮▮▮⚝ Q_OS_IOS: iOS 平台

    Qt 版本宏
    ▮▮▮▮⚝ QT_VERSION_MAJOR: Qt 主版本号
    ▮▮▮▮⚝ QT_VERSION_MINOR: Qt 次版本号
    ▮▮▮▮⚝ QT_VERSION_PATCH: Qt 修订版本号
    ▮▮▮▮⚝ QT_VERSION_STR: Qt 版本号字符串

    可以使用这些预定义宏,在代码中编写平台特定的代码逻辑。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QDebug>
    2
    3 void platformSpecificFunction() {
    4 #ifdef Q_OS_WINDOWS
    5 qDebug() << "This is Windows platform.";
    6 // Windows specific code
    7 #elif defined(Q_OS_MACOS)
    8 qDebug() << "This is macOS platform.";
    9 // macOS specific code
    10 #elif defined(Q_OS_LINUX)
    11 qDebug() << "This is Linux platform.";
    12 // Linux specific code
    13 #else
    14 qDebug() << "Unknown platform.";
    15 #endif
    16 }

    在这个例子中,我们使用 #ifdef, #elif, #else, #endif 预处理指令,根据不同的平台宏,编译不同的代码段。在 Windows 平台编译时,会编译 #ifdef Q_OS_WINDOWS#elif defined(Q_OS_MACOS) 之间的代码;在 macOS 平台编译时,会编译 #elif defined(Q_OS_MACOS)#elif defined(Q_OS_LINUX) 之间的代码;以此类推。

    条件编译的优点是:

    ▮ 可以将平台特定的代码和通用代码放在同一个源文件中,方便代码管理。
    ▮ 编译时根据平台选择性编译代码,可以减少最终可执行文件的大小。
    ▮ 可以根据 Qt 版本选择性编译代码,处理不同 Qt 版本之间的兼容性问题。

    条件编译的缺点是:

    ▮ 代码可读性较差,平台特定的代码和通用代码混合在一起,不易理解和维护。
    ▮ 编译时才能发现平台特定的错误,运行时错误可能会延迟到部署时才暴露出来。
    ▮ 过度使用条件编译可能会导致代码过于复杂和难以维护。

    平台特性检测 (Platform Feature Detection)

    平台特性检测是指在运行时检测当前平台是否支持某些特性或功能,然后根据检测结果执行不同的代码逻辑。Qt 框架提供了一些 API,可以用于检测平台特性,例如:

    QSysInfoQSysInfo 类提供了访问系统信息的 API,例如操作系统类型、操作系统版本、CPU 架构等。可以使用 QSysInfo::productType(), QSysInfo::productVersion() 等函数获取操作系统信息。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QSysInfo>
    2 #include <QDebug>
    3
    4 void checkOperatingSystem() {
    5 qDebug() << "Operating system:" << QSysInfo::prettyProductName();
    6 qDebug() << "Operating system type:" << QSysInfo::productType();
    7 qDebug() << "Operating system version:" << QSysInfo::productVersion();
    8
    9 if (QSysInfo::productType() == "windows") {
    10 qDebug() << "Running on Windows.";
    11 // Windows specific code
    12 } else if (QSysInfo::productType() == "osx") {
    13 qDebug() << "Running on macOS.";
    14 // macOS specific code
    15 } else if (QSysInfo::productType() == "linux") {
    16 qDebug() << "Running on Linux.";
    17 // Linux specific code
    18 }
    19 }

    QOpenGLContext::openGLModuleType() 函数:可以使用 QOpenGLContext::openGLModuleType() 函数检测当前平台使用的 OpenGL 模块类型,例如 Native OpenGL, Angle, Software Raster 等。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 #include <QOpenGLContext>
    2 #include <QDebug>
    3
    4 void checkOpenGLType() {
    5 QOpenGLContext *context = QOpenGLContext::currentContext();
    6 if (context) {
    7 QOpenGLContext::OpenGLModuleType type = context->openGLModuleType();
    8 if (type == QOpenGLContext::LibGL) {
    9 qDebug() << "Using Native OpenGL.";
    10 } else if (type == QOpenGLContext::LibANGLE) {
    11 qDebug() << "Using Angle OpenGL.";
    12 } else if (type == QOpenGLContext::Software) {
    13 qDebug() << "Using Software Rasterizer.";
    14 }
    15 } else {
    16 qDebug() << "No OpenGL context available.";
    17 }
    18 }

    平台特性检测的优点是:

    ▮ 代码可读性较好,平台特定的代码逻辑和通用代码逻辑分离,易于理解和维护。
    ▮ 运行时根据平台特性动态选择代码路径,更加灵活和适应性强。
    ▮ 可以更好地处理平台特性的差异,提供更佳的用户体验。

    平台特性检测的缺点是:

    ▮ 运行时检测特性会增加一些性能开销,虽然通常可以忽略不计。
    ▮ 需要在运行时处理平台特性差异,代码逻辑可能会更加复杂。

    在实际开发中,可以根据具体的应用场景和需求,选择使用条件编译或平台特性检测技术,或者将两者结合使用。对于简单的平台差异性处理,可以使用条件编译;对于复杂的平台特性检测和动态代码选择,可以使用平台特性检测。

    15.3.3 统一界面风格:Qt Style Sheets, 平台原生风格

    介绍如何使用 Qt Style Sheets 或平台原生风格,统一不同平台下的界面风格。

    界面风格是 GUI 应用程序用户体验的重要组成部分。为了实现更好的跨平台用户体验,需要尽可能地统一不同平台下的界面风格,避免用户在不同平台上使用应用程序时感到不一致和不适应。Qt 提供了两种主要的方式来统一界面风格:Qt Style Sheets (样式表) 和平台原生风格。

    Qt Style Sheets (样式表)

    Qt Style Sheets (QSS) 是一种类似于 CSS (Cascading Style Sheets) 的样式表语言,用于自定义 Qt 控件的外观。可以使用 QSS 设置控件的颜色、字体、背景、边框、布局等样式属性,实现高度定制化的界面风格。QSS 的优点是:

    高度定制化:可以精细地控制每个控件的样式属性,实现各种各样的界面风格。
    跨平台统一:QSS 样式在不同平台上通常表现一致,可以实现跨平台统一的界面风格。
    易于维护:QSS 样式可以集中管理,方便修改和维护。
    动态样式:可以在运行时动态修改 QSS 样式,实现界面风格的动态切换。

    使用 QSS 设置控件样式的方法主要有两种:

    在代码中设置样式:可以使用 QWidget::setStyleSheet() 函数为单个控件或整个窗口设置 QSS 样式。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 QPushButton *button = new QPushButton("Click me");
    2 button->setStyleSheet("QPushButton {"
    3 " background-color: lightblue;"
    4 " border: 1px solid blue;"
    5 " border-radius: 5px;"
    6 " padding: 5px;"
    7 "}"
    8 "QPushButton:hover {"
    9 " background-color: skyblue;"
    10 "}");

    使用 Qt Designer 设置样式:可以在 Qt Designer 中直接为控件设置 QSS 样式。在 Qt Designer 的属性编辑器中,找到 styleSheet 属性,点击右侧的 "..." 按钮,即可打开样式表编辑器,输入 QSS 样式代码。

    QSS 的语法和 CSS 类似,但也有一些 Qt 特有的扩展和限制。可以使用 Qt 助手 (Qt Assistant) 查看 QSS 的详细文档,了解 QSS 的语法和属性。

    平台原生风格 (Native Style)

    平台原生风格是指使用当前操作系统默认的界面风格。Qt 框架支持使用平台原生风格,可以使用 QApplication::setStyle(QStyleFactory::create("native")) 函数设置应用程序的界面风格为平台原生风格。平台原生风格的优点是:

    平台一致性:应用程序的界面风格与当前操作系统和其他应用程序的风格一致,用户感到熟悉和自然。
    性能较好:平台原生风格通常由操作系统直接渲染,性能较好。

    平台原生风格的缺点是:

    跨平台不统一:不同平台的原生风格差异较大,应用程序在不同平台上的界面风格不一致。
    定制性较差:平台原生风格的定制性较差,难以实现高度定制化的界面风格。

    在实际开发中,可以根据具体的应用场景和需求,选择使用 QSS 或平台原生风格,或者将两者结合使用。

    对于追求界面风格高度统一和定制化的应用程序,例如专业的设计软件或游戏,可以使用 QSS 自定义界面风格,实现跨平台一致的视觉效果。可以使用一些流行的 QSS 风格库,例如 Fusion 风格,它是一种跨平台通用的现代风格。
    对于追求平台原生体验和性能的应用程序,例如系统工具或实用程序,可以使用平台原生风格,使应用程序的界面风格与当前操作系统保持一致。

    可以根据应用程序的类型和目标用户群体,权衡选择 QSS 和平台原生风格,或者根据不同的平台选择不同的风格策略。例如,在 Windows 和 macOS 平台上使用平台原生风格,在 Linux 平台上使用 QSS 风格,以兼顾平台原生体验和跨平台统一性。

    15.4 前沿技术趋势:Qt 6 新特性、现代 C++ GUI 开发

    展望 C++ GUI 编程的前沿技术趋势,介绍 Qt 6 的新特性,以及现代 C++ GUI 开发的最佳实践。

    C++ GUI 编程领域在不断发展和演进。Qt 框架作为 C++ GUI 编程的主流框架,也在不断地推陈出新,引入新的特性和技术,以适应新的需求和挑战。本节将展望 C++ GUI 编程的前沿技术趋势,介绍 Qt 6 的新特性,以及现代 C++ GUI 开发的最佳实践。

    15.4.1 Qt 6 新特性:QML 引擎改进、图形渲染引擎 (RHI), C++ 模块更新

    介绍 Qt 6 的新特性,如 QML 引擎改进、图形渲染引擎 (RHI) 和 C++ 模块更新等。

    Qt 6 是 Qt 框架的最新主要版本,它在 Qt 5 的基础上进行了大量的改进和优化,引入了许多新的特性和功能,旨在提供更现代、更高效、更强大的 GUI 开发体验。Qt 6 的主要新特性包括:

    QML 引擎改进 (New QML Engine)

    Qt 6 使用了全新的 QML 引擎,称为 LTCG (Link-Time Code Generation) JIT (Just-In-Time) 引擎。新的 QML 引擎在性能、内存占用和功能方面都有显著提升:

    性能提升:新的 QML 引擎使用了 JIT 编译技术,可以将 QML 代码动态编译成机器码,提高 QML 代码的执行效率,尤其是在复杂的 QML 场景下,性能提升更加明显。
    内存占用降低:新的 QML 引擎在内存管理方面进行了优化,减少了 QML 场景的内存占用,尤其是在大型 QML 应用程序中,内存占用降低更加显著。
    新特性支持:新的 QML 引擎支持更多的 ECMAScript 新特性 (例如 ES6+),提供了更强大的 JavaScript 功能。
    更好的工具支持:新的 QML 引擎提供了更好的工具支持,例如更好的 QML 调试器和性能分析工具,方便开发者调试和优化 QML 代码。

    图形渲染引擎 (Rendering Hardware Interface, RHI)

    Qt 6 引入了新的图形渲染引擎 RHI (Rendering Hardware Interface)。RHI 是一个统一的图形 API 抽象层,它允许 Qt 应用程序使用不同的底层图形 API 进行渲染,例如:

    Direct3D 11/12 (Windows 平台)
    Metal (macOS 和 iOS 平台)
    Vulkan (跨平台通用图形 API)
    OpenGL (兼容性图形 API)

    RHI 的优点是:

    更好的性能:RHI 可以利用现代图形 API 的特性,例如命令列表、多线程渲染等,提高图形渲染性能,尤其是在高性能图形应用程序和游戏中,性能提升更加明显。
    更好的跨平台兼容性:RHI 可以根据当前平台选择最佳的图形 API 进行渲染,提供更好的跨平台兼容性和性能。
    更易于扩展:RHI 架构更加模块化和可扩展,方便添加新的图形 API 后端。

    Qt 6 仍然支持 OpenGL,但 OpenGL 已经被视为兼容性 API,推荐在新项目中使用 RHI 进行图形渲染,以获得更好的性能和前瞻性。

    C++ 模块更新 (C++ Module Updates)

    Qt 6 在 C++ 模块方面也进行了一些更新和改进:

    模块精简:Qt 6 移除了一些过时或不常用的模块,例如 Qt Script, Qt Declarative (Qt Quick 1) 等,使 Qt 框架更加精简和现代化。
    模块重组:Qt 6 对一些模块进行了重组和拆分,例如将 Qt Multimedia 模块拆分成多个子模块,更清晰地组织模块结构。
    C++ 标准更新:Qt 6 要求使用 C++17 标准进行编译,并开始逐步采用 C++20 标准的新特性,例如 Concepts, Modules 等,提供更现代的 C++ 编程体验。
    API 改进:Qt 6 对一些 C++ API 进行了改进和优化,例如改进了容器类的性能、添加了新的算法和工具类等。

    除了以上主要新特性,Qt 6 还包括许多其他的改进和优化,例如:

    CMake 构建系统:Qt 6 推荐使用 CMake 构建系统,取代了之前的 qmake 构建系统。CMake 是一个更现代、更强大、更灵活的构建系统,可以更好地支持大型项目和跨平台构建。
    改进的国际化 (Internationalization) 支持:Qt 6 改进了国际化支持,提供了更好的 Unicode 支持和本地化工具。
    性能优化:Qt 6 在多个方面进行了性能优化,例如启动速度优化、内存占用优化、渲染性能优化等。

    Qt 6 是 Qt 框架的一个重要里程碑,它代表了 Qt 框架的未来发展方向。对于新的 Qt 项目,强烈推荐使用 Qt 6 进行开发,以充分利用 Qt 6 的新特性和优势。对于 Qt 5 项目,可以逐步迁移到 Qt 6,以获得更好的性能和更现代的开发体验。

    15.4.2 现代 C++ GUI 开发实践:CMake 构建系统、模块化设计、设计模式应用

    探讨现代 C++ GUI 开发的最佳实践,如使用 CMake 构建系统、模块化设计、设计模式应用。

    现代 C++ GUI 开发不仅仅是学习 GUI 框架的 API,更重要的是掌握现代软件开发的理念和实践,构建高质量、可维护、可扩展的 GUI 应用程序。以下是一些现代 C++ GUI 开发的最佳实践:

    CMake 构建系统 (CMake Build System)

    CMake 是一个跨平台的开源构建系统,用于管理软件构建过程。CMake 使用简单的文本文件 (CMakeLists.txt) 描述项目的构建规则,然后可以生成各种平台 (例如 Visual Studio, Xcode, Makefile, Ninja 等) 的构建文件,实现跨平台构建。Qt 6 推荐使用 CMake 构建系统,CMake 的优点是:

    跨平台性:CMake 支持多种平台和构建工具,可以实现真正的跨平台构建。
    灵活性:CMake 提供了丰富的命令和模块,可以灵活地配置和定制构建过程。
    可扩展性:CMake 支持模块化和可扩展的架构,可以方便地添加自定义构建逻辑和第三方库。
    易于集成:CMake 可以与各种 IDE (集成开发环境) 和工具链集成,例如 Qt Creator, Visual Studio, Xcode, CLion 等。
    现代构建实践:CMake 遵循现代构建实践,例如 Out-of-Source 构建、依赖管理、测试集成等。

    对于新的 Qt 项目,强烈推荐使用 CMake 构建系统。Qt Creator 对 CMake 提供了良好的支持,可以方便地创建、配置和管理 CMake 项目。迁移到 CMake 构建系统可以带来更好的跨平台构建体验和更现代的构建实践。

    模块化设计 (Modular Design)

    模块化设计是指将应用程序分解成多个独立的、可重用的模块。每个模块负责完成特定的功能,模块之间通过清晰的接口进行交互。模块化设计的优点是:

    提高代码可重用性:可以将通用的功能模块提取出来,在不同的项目或模块中重用,减少代码重复编写。
    提高代码可维护性:模块化的代码结构更清晰,模块之间的依赖关系更明确,方便代码的维护和修改。
    提高代码可扩展性:可以方便地添加新的模块或替换现有的模块,扩展应用程序的功能。
    提高团队协作效率:不同的开发人员可以并行开发不同的模块,提高团队协作效率。
    降低代码耦合度:模块之间通过接口进行交互,降低了模块之间的耦合度,提高了代码的灵活性和可测试性。

    在 GUI 应用程序开发中,可以将用户界面 (View)、业务逻辑 (Controller)、数据模型 (Model) 分解成独立的模块,采用 Model-View-Controller (MVC)Model-View-ViewModel (MVVM) 等设计模式,实现模块化设计。

    设计模式应用 (Design Pattern Application)

    设计模式是在软件设计中反复出现的问题的通用解决方案。应用设计模式可以提高代码的质量、可重用性、可维护性和可扩展性。在 GUI 应用程序开发中,常用的设计模式包括:

    创建型模式 (Creational Patterns):例如单例模式 (Singleton), 工厂模式 (Factory), 抽象工厂模式 (Abstract Factory), 建造者模式 (Builder), 原型模式 (Prototype), 对象池模式 (Object Pool) 等。用于管理对象的创建过程,提高对象的创建效率和灵活性。
    结构型模式 (Structural Patterns):例如适配器模式 (Adapter), 桥接模式 (Bridge), 组合模式 (Composite), 装饰器模式 (Decorator), 外观模式 (Facade), 享元模式 (Flyweight), 代理模式 (Proxy) 等。用于组织类和对象的结构,提高代码的灵活性和可扩展性。
    行为型模式 (Behavioral Patterns):例如策略模式 (Strategy), 模板方法模式 (Template Method), 观察者模式 (Observer), 迭代器模式 (Iterator), 责任链模式 (Chain of Responsibility), 命令模式 (Command), 备忘录模式 (Memento), 状态模式 (State), 访问者模式 (Visitor), 中介者模式 (Mediator), 解释器模式 (Interpreter) 等。用于描述对象之间的交互和职责分配,提高代码的灵活性和可扩展性。

    在 GUI 应用程序开发中,可以根据具体的场景和需求,选择合适的设计模式,例如使用观察者模式实现模型与视图之间的解耦,使用策略模式实现不同的算法或策略的切换,使用工厂模式创建不同类型的控件或对象等。

    其他现代 C++ 实践

    除了以上最佳实践,还有一些其他的现代 C++ 实践,可以应用于 GUI 应用程序开发:

    使用 C++17/20 标准:Qt 6 要求使用 C++17 标准,并逐步支持 C++20 标准的新特性。使用现代 C++ 标准可以利用新的语言特性,例如 auto 类型推导, 范围 for 循环, 结构化绑定, Concepts, Modules 等,提高代码的简洁性、可读性和效率。
    代码审查 (Code Review):进行代码审查可以及早发现代码中的问题,提高代码质量,促进团队知识共享。可以使用代码审查工具,例如 Gerrit, Review Board, GitHub Pull Request 等。
    单元测试 (Unit Testing):编写单元测试可以验证代码的正确性,提高代码的可靠性和可维护性。Qt 提供了 Qt Test 框架,可以方便地编写和运行单元测试。
    持续集成 (Continuous Integration, CI):使用持续集成系统,例如 Jenkins, GitLab CI, GitHub Actions 等,可以自动化构建、测试和部署过程,提高开发效率和软件质量。
    代码静态分析 (Static Code Analysis):使用代码静态分析工具,例如 Clang Static Analyzer, PVS-Studio, SonarQube 等,可以自动检测代码中的潜在问题,例如内存泄漏, 空指针解引用, 代码风格违规等,提高代码质量和安全性。

    通过应用这些现代 C++ GUI 开发的最佳实践,可以构建更高质量、更可维护、更可扩展的 GUI 应用程序,提升开发效率和用户体验。

    15.4.3 GUI 开发未来趋势:WebAssembly, 跨平台框架演进

    展望 GUI 开发的未来趋势,如 WebAssembly 和跨平台框架的演进。

    GUI 开发领域在不断发展和演进,新的技术和趋势不断涌现。以下是一些 GUI 开发的未来趋势展望:

    WebAssembly (Wasm)

    WebAssembly (Wasm) 是一种新的Web 标准,它定义了一种可移植的、大小和加载时间优化的二进制指令格式。Wasm 最初被设计为 Web 浏览器上的代码执行引擎,但其跨平台、高性能、安全等特性,使其逐渐扩展到 Web 浏览器之外的应用领域,包括 GUI 开发。Wasm 在 GUI 开发领域的潜力主要体现在:

    跨平台性:Wasm 代码可以在任何支持 Wasm 虚拟机的平台上运行,包括 Web 浏览器、桌面操作系统、移动设备、嵌入式系统等。这使得使用 Wasm 开发的 GUI 应用程序具有天然的跨平台性,可以实现 “一次编写,到处运行” 的目标。
    高性能:Wasm 是一种二进制指令格式,可以被虚拟机高效地执行。Wasm 代码的执行性能接近原生代码,远高于传统的 JavaScript 代码。这使得使用 Wasm 开发的 GUI 应用程序可以获得接近原生应用程序的性能体验,尤其是在图形渲染、动画效果、复杂计算等方面。
    安全性:Wasm 代码运行在沙箱环境中,与宿主环境隔离,具有较高的安全性。这使得使用 Wasm 开发的 GUI 应用程序可以安全地运行在 Web 浏览器等安全敏感的环境中。
    语言灵活性:Wasm 并非一种编程语言,而是一种虚拟机指令格式。可以使用多种编程语言 (例如 C++, Rust, C#, Go 等) 编译生成 Wasm 代码。这使得开发者可以使用自己熟悉的编程语言进行 GUI 开发,并利用 Wasm 的跨平台和高性能特性。

    Qt 框架已经开始支持 WebAssembly。Qt for WebAssembly 允许将使用 Qt Widgets 或 Qt Quick 开发的 GUI 应用程序编译成 Wasm 代码,并在 Web 浏览器中运行。Qt for WebAssembly 的实现原理主要是:

    使用 Emscripten 编译器工具链:Emscripten 是一个将 C/C++ 代码编译成 WebAssembly 和 JavaScript 的工具链。Qt for WebAssembly 使用 Emscripten 将 Qt 框架和应用程序代码编译成 Wasm 代码。
    Qt 核心库的 Wasm 移植:Qt 核心库 (QtCore, QtGui, QtWidgets, QtQuick 等) 已经被移植到 WebAssembly 平台,可以在 Wasm 虚拟机中运行。
    浏览器 API 的桥接:Qt for WebAssembly 使用 JavaScript 代码桥接浏览器 API,例如 DOM (文档对象模型, Document Object Model), Canvas (画布), WebGL (Web 图形库, Web Graphics Library), WebAudio (Web 音频 API, Web Audio API) 等,使得 Wasm 代码可以访问浏览器提供的功能。

    使用 Qt for WebAssembly 开发 GUI 应用程序的流程与传统的 Qt 开发流程类似,主要区别在于编译和部署环节。使用 Qt for WebAssembly,可以将 Qt 应用程序编译成 .wasm 文件和相关的 JavaScript 和 HTML 文件,然后将这些文件部署到 Web 服务器上,用户可以使用 Web 浏览器访问和运行应用程序。

    尽管 WebAssembly 在 GUI 开发领域具有巨大的潜力,但目前仍处于发展初期,存在一些挑战和限制:

    浏览器沙箱限制:Wasm 代码运行在浏览器沙箱环境中,受到浏览器的安全限制,例如无法直接访问本地文件系统、操作系统 API 等。虽然可以使用浏览器提供的 API 进行有限的访问,但与原生应用程序相比,功能仍然受限。
    DOM 交互复杂性:WebAssembly 与 Web 浏览器交互主要通过 JavaScript 和 DOM API 进行桥接,这会增加开发的复杂性,并可能带来性能开销。直接操作 DOM 元素的性能通常不如原生 GUI 框架。
    Wasm GUI 生态系统不成熟:与成熟的原生 GUI 框架 (例如 Qt, Win32 API, Cocoa, GTK+) 相比,WebAssembly GUI 生态系统尚不成熟,缺乏完善的 GUI 库、控件库、工具链和社区支持。

    尽管存在这些挑战,但随着 WebAssembly 技术的不断发展和完善,以及 WebAssembly GUI 生态系统的逐步成熟,WebAssembly 有望成为未来跨平台 GUI 开发的重要技术方向之一,尤其是在 Web 应用程序、在线应用服务、轻量级桌面应用等领域。

    跨平台框架演进 (Evolution of Cross-Platform Frameworks)

    跨平台 GUI 框架一直是 GUI 开发领域的重要组成部分。随着移动互联网、云计算、Web 技术的快速发展,跨平台 GUI 框架也在不断演进和发展,以适应新的需求和挑战。跨平台框架的演进趋势主要体现在以下几个方面:

    更高的性能追求:早期的跨平台框架 (例如 Java Swing, Adobe Flash/AIR) 通常采用解释型语言或虚拟机技术,性能相对较低。现代跨平台框架 (例如 Qt, React Native, Flutter) 更加注重性能,采用编译型语言、原生控件渲染、硬件加速等技术,力求提供接近原生应用程序的性能体验。WebAssembly 的出现也为跨平台框架提供了新的性能提升途径。
    更友好的开发体验:现代跨平台框架更加注重开发者的开发体验,提供更简洁、更高效、更易用的 API, 工具链和开发模式。例如,声明式 UI 框架 (例如 QML, React, Flutter) 的兴起,大大简化了 UI 开发的复杂性,提高了开发效率。热重载 (Hot Reload), 代码编辑器集成, 可视化设计工具等现代开发工具也极大地提升了开发效率。
    Web 技术融合:Web 技术 (例如 HTML, CSS, JavaScript, Web Components) 在 UI 开发领域的影响力越来越大。现代跨平台框架也更加注重与 Web 技术的融合,例如 Electron 框架直接基于 Web 技术栈 (Chromium + Node.js) 构建跨平台桌面应用,React Native 和 Flutter 也借鉴了 Web 开发的组件化、声明式 UI 等理念。WebAssembly 的出现也为 Web 技术和原生技术融合提供了新的可能性。
    组件化和模块化:组件化和模块化是现代软件开发的重要趋势。跨平台框架也更加注重组件化和模块化设计,提供丰富的组件库、控件库和模块化架构,方便开发者构建可重用、可维护、可扩展的 GUI 应用程序。例如,Qt Quick Controls, React Native Components, Flutter Widgets 等组件库的出现,大大简化了 UI 组件的开发和使用。
    声明式 UI 成为主流:声明式 UI 编程范式 (例如 QML, React, Flutter, SwiftUI, Jetpack Compose) 逐渐成为现代 GUI 开发的主流趋势。声明式 UI 以数据驱动视图更新,代码更简洁、更易读、更易维护,开发效率更高。与传统的命令式 UI 编程相比,声明式 UI 更加符合现代 UI 开发的需求。

    展望未来,跨平台 GUI 框架将继续朝着更高性能、更友好开发体验、更深度 Web 技术融合、更完善组件化和模块化、更普及声明式 UI 等方向演进。各种跨平台框架将会在不同的应用领域和场景中发挥各自的优势,共同推动 GUI 开发技术的进步。例如:

    Qt 框架:将继续在高性能、跨平台、原生体验的 C++ GUI 开发领域保持领先地位,并在 Qt 6 基础上进一步完善 RHI 渲染引擎、QML 引擎、C++ 模块,并积极探索 WebAssembly 等新技术。Qt 框架在桌面应用、嵌入式系统、工业控制、汽车电子等领域具有广泛的应用前景。
    Electron 框架:将继续在跨平台桌面 Web 应用领域保持主导地位,利用 Web 技术的成熟生态系统和庞大开发者社区,快速构建跨平台桌面应用程序。Electron 框架在开发工具、通讯软件、内容创作等领域具有广泛的应用。
    React Native 和 Flutter 框架:将在跨平台移动应用开发领域发挥重要作用,利用 JavaScript (React Native) 和 Dart (Flutter) 语言的开发效率和跨平台能力,快速构建高性能、高颜值的移动应用程序。React Native 和 Flutter 框架在社交应用、电商应用、生活服务应用等领域具有广泛的应用。
    WebAssembly GUI 框架:将会在 Web 应用程序、在线应用服务、轻量级桌面应用等领域逐渐兴起,利用 WebAssembly 的跨平台、高性能、安全等特性,构建新一代的跨平台 GUI 应用程序。

    各种跨平台框架之间也会相互借鉴、相互融合,例如 Web 框架借鉴原生框架的性能优化技术,原生框架借鉴 Web 框架的组件化、声明式 UI 等理念。未来跨平台 GUI 开发领域将会更加多元化、更加繁荣发展。开发者可以根据具体的项目需求、技术栈偏好、目标平台等因素,选择合适的跨平台框架,并持续关注 GUI 开发领域的新技术和新趋势,不断提升自己的技能和竞争力。

    Appendix A: 附录 A:Qt 常用类速查表

    Appendix A1: 核心模块 (QtCore) 常用类速查

    QObject: Qt 对象模型的核心类,是所有支持信号与槽机制的类的基类。
    ▮▮▮▮ⓑ 负责对象生命周期管理、信号与槽机制、属性系统等核心功能。
    ▮▮▮▮ⓒ 在 Qt 中,几乎所有与 GUI 相关的类都直接或间接继承自 QObject

    QString: Unicode 字符串类,用于处理文本数据。
    ▮▮▮▮ⓑ 支持 Unicode 编码,可以方便地进行字符串操作,如拼接、查找、替换等。
    ▮▮▮▮ⓒ Qt 中所有涉及到文本处理的地方都广泛使用 QString

    QList: 通用模板列表类,提供动态数组的功能。
    ▮▮▮▮ⓑ 可以存储任意类型的对象 (T),并提供高效的插入、删除、访问等操作。
    ▮▮▮▮ⓒ 是 Qt 中常用的容器类之一。

    QMap: 通用模板关联容器类,提供键值对的存储和查找功能。
    ▮▮▮▮ⓑ 键 (Key) 必须是可排序的,值 (T) 可以是任意类型。
    ▮▮▮▮ⓒ 根据键快速查找对应的值。

    QVariant: 万能数据类型,可以存储各种不同的 Qt 数据类型。
    ▮▮▮▮ⓑ 用于在不同类型之间传递数据,例如在属性系统和信号与槽机制中。
    ▮▮▮▮ⓒ 可以存储 int, bool, QString, QList, QMap 等多种类型。

    QTimer: 定时器类,用于在指定时间间隔后触发信号。
    ▮▮▮▮ⓑ 可以设置单次定时或周期性定时。
    ▮▮▮▮ⓒ 常用于动画、定时任务等场景。

    QSettings: 应用程序配置管理类,用于读写应用程序的配置信息。
    ▮▮▮▮ⓑ 可以将配置信息存储在注册表 (Windows)、INI 文件或 XML 文件中。
    ▮▮▮▮ⓒ 方便应用程序保存和加载用户设置。

    QDir: 目录操作类,用于处理文件系统中的目录。
    ▮▮▮▮ⓑ 可以创建、删除、遍历目录,获取目录信息等。
    ▮▮▮▮ⓒ 常用于文件管理相关的应用。

    QFile: 文件操作类,用于处理文件系统中的文件。
    ▮▮▮▮ⓑ 可以打开、关闭、读写文件,获取文件信息等。
    ▮▮▮▮ⓒ 是进行文件 I/O 操作的基础类。

    QTextStream: 文本流类,提供方便的文本文件读写接口。
    ▮▮▮▮ⓑ 可以按行、按字符读写文本文件,支持格式化输出。
    ▮▮▮▮ⓒ 比 QDataStream 更适合处理文本数据。

    QDataStream: 数据流类,提供二进制数据文件读写接口。
    ▮▮▮▮ⓑ 可以读写各种基本数据类型和 Qt 数据类型,保持数据类型信息。
    ▮▮▮▮ⓒ 适合存储结构化数据。

    Appendix A2: 图形界面模块 (QtGui) 常用类速查

    QGuiApplication: GUI 应用程序类,用于管理 GUI 应用程序的生命周期和全局设置。
    ▮▮▮▮ⓑ 是基于 QtGui 的应用程序的入口点,负责初始化和事件循环。
    ▮▮▮▮ⓒ 例如处理命令行参数、设置应用程序样式等。

    QScreen: 屏幕类,代表一个物理屏幕。
    ▮▮▮▮ⓑ 可以获取屏幕的尺寸、分辨率、DPI (每英寸点数) 等信息。
    ▮▮▮▮ⓒ 用于多显示器环境下的编程。

    QCursor: 鼠标光标类,用于控制鼠标光标的形状和位置。
    ▮▮▮▮ⓑ 可以设置自定义光标形状,例如箭头、十字星、等待光标等。
    ▮▮▮▮ⓒ 响应用户交互时改变光标反馈。

    QClipboard: 剪贴板类,用于访问系统剪贴板。
    ▮▮▮▮ⓑ 可以进行文本、图像等数据的复制和粘贴操作。
    ▮▮▮▮ⓒ 实现应用程序与系统剪贴板的交互。

    QDrag: 拖放操作类,用于实现拖放功能。
    ▮▮▮▮ⓑ 允许用户通过拖拽在应用程序内部或应用程序之间传递数据。
    ▮▮▮▮ⓒ 提供用户友好的数据交互方式。

    QFont: 字体类,用于设置文本的字体属性。
    ▮▮▮▮ⓑ 可以设置字体族、字号、字重、斜体、下划线等属性。
    ▮▮▮▮ⓒ 控制文本的显示样式。

    QColor: 颜色类,用于表示颜色。
    ▮▮▮▮ⓑ 支持 RGB (红绿蓝)、HSV (色相饱和度明度)、CMYK (青品黄黑) 等颜色模型。
    ▮▮▮▮ⓒ 用于设置控件的颜色、绘制图形的颜色等。

    QPalette: 调色板类,用于管理控件的颜色方案。
    ▮▮▮▮ⓑ 定义了控件在不同状态下的颜色,例如正常状态、禁用状态、选中状态等。
    ▮▮▮▮ⓒ 可以统一设置应用程序的整体颜色风格。

    QIcon: 图标类,用于表示应用程序的图标或控件上的图标。
    ▮▮▮▮ⓑ 可以加载不同尺寸和状态的图标,适应不同场景。
    ▮▮▮▮ⓒ 增强用户界面的视觉效果。

    QImage: 图像类,用于处理像素级别的图像数据。
    ▮▮▮▮ⓑ 支持多种图像格式,可以进行图像加载、保存、像素操作等。
    ▮▮▮▮ⓒ 适合进行图像处理和编辑。

    QPixmap: 平台相关的图像类,用于在屏幕上显示图像。
    ▮▮▮▮ⓑ 针对不同平台进行了优化,显示效率更高。
    ▮▮▮▮ⓒ 常用于显示静态图像。

    QBitmap: 位图类,用于表示单色图像。
    ▮▮▮▮ⓑ 仅包含两种颜色,常用于创建掩码或简单的图形元素。
    ▮▮▮▮ⓒ 节省内存,绘制效率高。

    QPainter: 画家类,提供 2D 绘图接口。
    ▮▮▮▮ⓑ 可以绘制各种图形、文本、图像,设置画笔、画刷、字体等属性。
    ▮▮▮▮ⓒ 是 Qt 绘图系统的核心类。

    QPen: 画笔类,用于设置线条的颜色、宽度、线型等属性。
    ▮▮▮▮ⓑ 在 QPainter 绘图时,用于绘制线条和图形的轮廓。
    ▮▮▮▮ⓒ 控制线条的视觉效果。

    QBrush: 画刷类,用于设置填充图形的颜色、图案等属性。
    ▮▮▮▮ⓑ 在 QPainter 绘图时,用于填充图形的内部区域。
    ▮▮▮▮ⓒ 控制填充效果,例如实心填充、渐变填充、图案填充等。

    Appendix A3: 控件模块 (QtWidgets) 常用类速查

    QApplication: 控件 (Widget) 应用程序类,是基于 QtWidgets 的应用程序的入口点。
    ▮▮▮▮ⓑ 继承自 QGuiApplication,添加了对控件的支持。
    ▮▮▮▮ⓒ 用于创建基于控件的 GUI 应用程序。

    QWidget: 所有控件的基类,代表用户界面中的一个可视元素。
    ▮▮▮▮ⓑ 具有窗口属性,可以接收用户输入,进行绘制等。
    ▮▮▮▮ⓒ 是构建用户界面的基本 building block (构建块)。

    QMainWindow: 主窗口类,提供应用程序的主窗口框架。
    ▮▮▮▮ⓑ 包含菜单栏 (QMenuBar)、工具栏 (QToolBar)、状态栏 (QStatusBar)、中央工作区 (Central Widget) 等。
    ▮▮▮▮ⓒ 适用于创建具有标准菜单和工具栏的应用程序。

    QDialog: 对话框类,用于创建各种对话框窗口。
    ▮▮▮▮ⓑ 例如模态对话框、非模态对话框、消息框、文件对话框、颜色对话框等。
    ▮▮▮▮ⓒ 用于与用户进行简短的交互。

    QLabel: 标签控件,用于显示文本或图像。
    ▮▮▮▮ⓑ 可以显示静态文本、富文本或图像。
    ▮▮▮▮ⓒ 常用于显示提示信息或标题。

    QPushButton: 按钮控件,用于触发用户操作。
    ▮▮▮▮ⓑ 当用户点击按钮时,会发射 clicked() 信号。
    ▮▮▮▮ⓒ 是用户界面中最常用的交互控件之一。

    QLineEdit: 单行文本输入框控件,用于接收单行文本输入。
    ▮▮▮▮ⓑ 提供文本编辑功能,例如剪切、复制、粘贴、撤销、重做等。
    ▮▮▮▮ⓒ 常用于输入用户名、密码、搜索关键词等。

    QTextEdit: 多行文本编辑框控件,用于显示和编辑多行文本。
    ▮▮▮▮ⓑ 支持富文本编辑,可以设置字体、颜色、段落格式等。
    ▮▮▮▮ⓒ 适用于创建文本编辑器、日志显示窗口等。

    QCheckBox: 复选框控件,用于进行多项选择。
    ▮▮▮▮ⓑ 可以选中或取消选中,通常用于表示布尔类型的选项。
    ▮▮▮▮ⓒ 用户可以选择多个选项。

    QRadioButton: 单选按钮控件,用于在多个选项中选择一个。
    ▮▮▮▮ⓑ 同一组单选按钮中只能有一个被选中。
    ▮▮▮▮ⓒ 用户只能选择一个选项。

    QComboBox: 下拉框控件,提供下拉列表供用户选择。
    ▮▮▮▮ⓑ 节省界面空间,提供选项列表。
    ▮▮▮▮ⓒ 常用于选择城市、国家、字体等。

    QListWidget: 列表框控件,显示项目列表供用户选择。
    ▮▮▮▮ⓑ 可以显示文本、图标或自定义的列表项。
    ▮▮▮▮ⓒ 适用于显示大量选项或复杂列表项。

    QSlider: 滑动条控件,用于在一定范围内选择数值。
    ▮▮▮▮ⓑ 用户可以通过拖动滑块来改变数值。
    ▮▮▮▮ⓒ 常用于调节音量、亮度、进度等。

    QSpinBox: 微调框控件,提供数值输入和微调功能。
    ▮▮▮▮ⓑ 可以通过箭头按钮或键盘输入来改变数值。
    ▮▮▮▮ⓒ 适用于精确数值输入。

    QProgressBar: 进度条控件,显示任务的进度。
    ▮▮▮▮ⓑ 可以显示任务完成的百分比或具体进度值。
    ▮▮▮▮ⓒ 提供任务执行的视觉反馈。

    QDial: 刻度盘控件,类似于旋钮,用于选择角度或数值。
    ▮▮▮▮ⓑ 用户可以通过旋转刻度盘来改变数值。
    ▮▮▮▮ⓒ 适用于模拟旋钮操作的界面。

    布局管理器 (Layout Managers): 用于自动管理控件的布局,使界面能够自适应窗口大小变化。
    ▮▮▮▮ⓑ QHBoxLayout: 水平布局管理器,将控件水平排列。
    ▮▮▮▮ⓒ QVBoxLayout: 垂直布局管理器,将控件垂直排列。
    ▮▮▮▮ⓓ QGridLayout: 网格布局管理器,将控件按网格排列。
    ▮▮▮▮ⓔ QFormLayout: 表单布局管理器,用于创建表单风格的布局。
    ▮▮▮▮ⓕ QStackedLayout: 堆叠布局管理器,用于实现页面切换效果。

    Appendix A4: 网络模块 (QtNetwork) 常用类速查

    QTcpSocket: TCP 套接字类,用于建立 TCP 连接,进行 TCP 数据传输。
    ▮▮▮▮ⓑ 用于创建 TCP 客户端,连接服务器并进行双向通信。
    ▮▮▮▮ⓒ 支持流式数据传输。

    QUdpSocket: UDP 套接字类,用于进行 UDP 数据报通信。
    ▮▮▮▮ⓑ 用于创建 UDP 客户端或服务器,进行无连接的数据报传输。
    ▮▮▮▮ⓒ 适用于实时性要求高,可靠性要求相对较低的场景,如音视频流传输。

    QTcpServer: TCP 服务器类,用于监听端口,接受客户端 TCP 连接。
    ▮▮▮▮ⓑ 创建 TCP 服务器端应用程序,等待客户端连接。
    ▮▮▮▮ⓒ 每当有客户端连接时,会发射 newConnection() 信号。

    QNetworkAccessManager: 网络访问管理器类,用于发送 HTTP 请求,处理网络资源。
    ▮▮▮▮ⓑ 可以发送 GET, POST, PUT, DELETE 等 HTTP 请求。
    ▮▮▮▮ⓒ 用于访问 Web API、下载网页资源等。

    QNetworkRequest: 网络请求类,封装 HTTP 请求的各种信息,如 URL (统一资源定位符), 请求头 (Headers) 等。
    ▮▮▮▮ⓑ 用于配置 QNetworkAccessManager 发送的请求。
    ▮▮▮▮ⓒ 可以设置请求方法、请求头、请求体等。

    QNetworkReply: 网络回复类,封装 HTTP 服务器的响应信息,如状态码 (Status Code), 响应头 (Headers), 响应体 (Body) 等。
    ▮▮▮▮ⓑ QNetworkAccessManager 发送请求后,服务器的响应会封装在 QNetworkReply 对象中。
    ▮▮▮▮ⓒ 可以获取响应状态、响应头、响应数据等。

    Appendix A5: SQL 模块 (QtSql) 常用类速查

    QSqlDatabase: 数据库连接类,用于管理数据库连接。
    ▮▮▮▮ⓑ 可以添加、移除、打开、关闭数据库连接。
    ▮▮▮▮ⓒ 应用程序通过 QSqlDatabase 对象与数据库建立连接。

    QSqlQuery: SQL 查询类,用于执行 SQL 语句,获取查询结果。
    ▮▮▮▮ⓑ 可以执行 SELECT, INSERT, UPDATE, DELETE 等 SQL 语句。
    ▮▮▮▮ⓒ 提供 next(), value() 等方法遍历查询结果。

    QSqlTableModel: SQL 表模型类,将数据库表数据映射到模型,方便在视图中显示和编辑。
    ▮▮▮▮ⓑ 继承自 QAbstractTableModel,可以直接操作数据库表数据。
    ▮▮▮▮ⓒ 适用于单表数据的显示和编辑。

    QSqlRelationalTableModel: SQL 关联表模型类,扩展了 QSqlTableModel,支持处理关联表的数据。
    ▮▮▮▮ⓑ 可以处理包含外键关系的表格数据。
    ▮▮▮▮ⓒ 适用于需要显示和编辑关联数据的场景。

    Appendix A6: Qt Quick/QML 常用元素速查

    Rectangle: 矩形元素,用于绘制矩形区域。
    ▮▮▮▮ⓑ 可以设置颜色、边框、圆角等属性。
    ▮▮▮▮ⓒ 常用于背景、按钮、容器等。

    Text: 文本元素,用于显示文本。
    ▮▮▮▮ⓑ 可以设置文本内容、字体、颜色、对齐方式等属性。
    ▮▮▮▮ⓒ 用于显示静态文本或动态文本。

    Image: 图像元素,用于显示图像。
    ▮▮▮▮ⓑ 可以加载本地或网络图像,设置缩放模式、透明度等属性。
    ▮▮▮▮ⓒ 用于显示图片、图标等。

    MouseArea: 鼠标区域元素,用于响应鼠标事件。
    ▮▮▮▮ⓑ 可以检测鼠标点击、移动、悬停等事件。
    ▮▮▮▮ⓒ 用于创建交互区域,例如按钮、链接等。

    定位器 (Positioners): 用于自动布局元素的元素。
    ▮▮▮▮ⓑ Row: 行定位器,将子元素水平排列。
    ▮▮▮▮ⓒ Column: 列定位器,将子元素垂直排列。
    ▮▮▮▮ⓓ Grid: 网格定位器,将子元素按网格排列。
    ▮▮▮▮ⓔ Flow: 流式定位器,将子元素按流式布局排列。

    动画 (Animations): 用于创建动画效果的元素。
    ▮▮▮▮ⓑ NumberAnimation: 数值动画,改变数值属性的动画。
    ▮▮▮▮ⓒ ColorAnimation: 颜色动画,改变颜色属性的动画。
    ▮▮▮▮ⓓ RotationAnimation: 旋转动画,改变旋转角度的动画。
    ▮▮▮▮ⓔ PropertyAnimation: 通用属性动画,可以动画任意属性。

    状态 (States): 用于管理不同状态下的属性值和动画效果。
    ▮▮▮▮ⓑ 可以定义多个状态,每个状态下元素具有不同的属性值。
    ▮▮▮▮ⓒ 可以通过状态切换实现复杂的界面效果。

    过渡 (Transitions): 用于在状态切换时添加平滑的动画效果。
    ▮▮▮▮ⓑ 可以定义状态切换时的属性动画,使界面过渡更加自然流畅。
    ▮▮▮▮ⓒ 增强用户体验。

    粒子效果 (Particle Effects): 用于创建炫酷的视觉效果,例如火焰、烟雾、星空等。
    ▮▮▮▮ⓑ 通过粒子发射器和粒子绘制器实现。
    ▮▮▮▮ⓒ 增加界面的吸引力。

    QQuickView: Qt Quick 视图类,用于加载和显示 QML 界面。
    ▮▮▮▮ⓑ 可以将 QML 界面嵌入到 C++ 应用程序中。
    ▮▮▮▮ⓒ 是 C++ 和 QML 混合编程的重要桥梁。

    Appendix B: 附录 B:C++ 基础知识回顾

    Summary: 摘要

    本附录旨在对 C++ 编程语言的基础知识进行回顾,特别是那些对于理解和应用 Qt 框架进行 GUI 编程至关重要的概念。本附录并非 C++ 语言的完整教程,而是为读者快速回顾关键知识点,确保具备顺利学习本书后续章节所需的 C++ 基础。

    B.1 C++ 语言基础

    B.1.1 基本语法 (Basic Syntax)

    程序结构: C++ 程序由一个或多个函数组成,其中 main() 函数是程序的入口点 (entry point)。
    语句 (Statement) 和表达式 (Expression): C++ 代码由语句构成,语句可以包含表达式。表达式用于计算值,语句则执行操作。
    注释 (Comment): 使用 // 进行单行注释,使用 /* ... */ 进行多行注释,用于提高代码可读性。
    分号 (Semicolon): C++ 中,大多数语句以分号 ; 结尾。
    代码块 (Code Block): 使用花括号 {} 包围的代码段,定义作用域 (scope)。

    B.1.2 数据类型 (Data Types)

    基本数据类型 (Primitive Data Types):
    int: 整型 (integer),用于表示整数。
    float: 单精度浮点型 (single-precision floating-point),用于表示浮点数。
    double: 双精度浮点型 (double-precision floating-point),用于表示更高精度的浮点数。
    char: 字符型 (character),用于表示单个字符。
    bool: 布尔型 (boolean),用于表示真 (true) 或假 (false) 的逻辑值。
    void: 空类型 (void type),用于表示无类型或无返回值。
    限定符 (Qualifiers):
    short, long: 用于修饰 int 类型,改变整数的范围。例如 short int, long int, long long int
    unsigned, signed: 用于修饰整型和字符型,指定是否为无符号类型。例如 unsigned int, signed char
    复合数据类型 (Compound Data Types):
    数组 (Array): 存储相同类型元素的集合。例如 int array[10];
    结构体 (Struct): 可以存储不同类型成员的集合。
    联合体 (Union): 允许多个变量共享同一块内存空间。
    枚举 (Enum): 创建具名的整型常量集合。
    指针 (Pointer): 存储变量内存地址的变量。例如 int *ptr;
    引用 (Reference): 已存在变量的别名。例如 int &ref = variable;

    B.1.3 变量 (Variables) 与常量 (Constants)

    变量声明 (Variable Declaration): 在使用变量前必须先声明其类型和名称。例如 int count;
    变量初始化 (Variable Initialization): 变量可以在声明时或之后初始化赋值。例如 int count = 0;count = 10;
    常量 (Constants): 值在程序执行期间不能被修改。
    字面常量 (Literal Constants): 直接出现在代码中的值,如 10, 3.14, 'a', "hello"
    const 关键字: 使用 const 关键字声明常量变量。例如 const int MAX_VALUE = 100;
    #define 预处理器指令: 使用 #define 定义符号常量。例如 #define PI 3.14159

    B.1.4 运算符 (Operators)

    算术运算符 (Arithmetic Operators): + (加), - (减), * (乘), / (除), % (取模)。
    关系运算符 (Relational Operators): == (等于), != (不等于), > (大于), < (小于), >= (大于等于), <= (小于等于)。
    逻辑运算符 (Logical Operators): && (逻辑与), || (逻辑或), ! (逻辑非)。
    赋值运算符 (Assignment Operators): = (赋值), +=, -=, *=, /=, %= (复合赋值)。
    位运算符 (Bitwise Operators): & (按位与), | (按位或), ^ (按位异或), ~ (按位取反), << (左移), >> (右移)。
    自增自减运算符 (Increment/Decrement Operators): ++ (自增), -- (自减)。前缀形式 (++i, --i) 和后缀形式 (i++, i--) 的区别。
    条件运算符 (Conditional Operator): ? AlBeRt63EiNsTeIn value_if_false;
    逗号运算符 (Comma Operator): , 用于分隔表达式,结果为最后一个表达式的值。
    作用域解析运算符 (Scope Resolution Operator): :: 用于访问全局作用域或类作用域的成员。

    B.1.5 控制流 (Control Flow)

    条件语句 (Conditional Statements):
    if 语句: 根据条件执行代码块。
    if-else 语句: 条件为真执行一个代码块,否则执行另一个代码块。
    if-else if-else 语句: 多条件分支选择。
    switch 语句: 根据表达式的值选择执行不同的 case 分支。
    循环语句 (Loop Statements):
    for 循环: 在已知循环次数或有明确循环条件时使用。
    while 循环: 当条件为真时重复执行代码块。
    do-while 循环: 先执行一次代码块,然后在条件为真时重复执行。
    跳转语句 (Jump Statements):
    break 语句: 跳出循环或 switch 语句。
    continue 语句: 跳过当前循环迭代的剩余部分,继续下一次迭代。
    goto 语句: 无条件跳转到标记位置 (通常应避免使用,因为它会降低代码可读性和可维护性)。
    return 语句: 从函数返回。

    B.1.6 函数 (Functions)

    函数定义 (Function Definition): 包括函数名、返回类型、参数列表和函数体。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 返回类型 函数名(参数列表) {
    2 // 函数体
    3 return 返回值; // 如果函数有返回值
    4 }

    函数声明 (Function Declaration)/函数原型 (Function Prototype): 在使用函数之前声明函数名、返回类型和参数列表,但不包含函数体。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 返回类型 函数名(参数列表); // 注意分号

    函数调用 (Function Call): 使用函数名和参数列表来执行函数。例如 functionName(arguments);
    参数传递 (Parameter Passing):
    值传递 (Pass-by-value): 将参数的值复制给函数的形式参数,函数内部对参数的修改不影响原始变量。
    指针传递 (Pass-by-pointer): 将参数的地址传递给函数,函数可以通过指针修改原始变量的值。
    引用传递 (Pass-by-reference): 将参数的引用传递给函数,函数内部对参数的修改会直接影响原始变量。
    函数重载 (Function Overloading): 在同一作用域内,可以定义多个函数名相同但参数列表不同的函数。
    inline 函数: 建议编译器将函数调用替换为函数体代码,以提高性能 (但不保证一定内联)。
    constexpr 函数: 在编译时求值的函数,用于编译时常量计算。
    递归函数 (Recursive Functions): 函数直接或间接调用自身。

    B.2 面向对象编程 (Object-Oriented Programming, OOP) 基础

    B.2.1 类 (Class) 与对象 (Object)

    类 (Class) 的定义: 类是创建对象的蓝图或模板,定义了对象的属性 (attributes/成员变量) 和行为 (behaviors/成员函数)。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 class 类名 {
    2 public: // 公有访问修饰符
    3 // 成员变量 (属性)
    4 // 成员函数 (方法)
    5 protected: // 保护访问修饰符
    6 // ...
    7 private: // 私有访问修饰符
    8 // ...
    9 }; // 注意分号

    访问修饰符 (Access Modifiers):
    public: 公有成员,可以在类的内部和外部访问。
    protected: 保护成员,可以在类的内部、派生类中访问,但不能在类的外部直接访问。
    private: 私有成员,只能在类的内部访问。
    对象 (Object) 的创建: 通过类实例化创建对象。例如 ClassName objectName;
    成员访问 (Member Access): 使用点运算符 . 访问对象的公有成员。例如 objectName.memberName;
    构造函数 (Constructor): 特殊的成员函数,在对象创建时自动调用,用于初始化对象。可以有多个重载的构造函数。
    析构函数 (Destructor): 特殊的成员函数,在对象销毁时自动调用,用于清理资源。一个类只能有一个析构函数。

    B.2.2 封装 (Encapsulation)

    概念: 将数据 (属性) 和操作数据的方法 (行为) 捆绑在一起,并对外部隐藏内部实现细节。
    实现: 通过类和访问修饰符实现封装。将数据成员设为 privateprotected,通过 public 成员函数提供对数据的访问和操作接口。
    优势: 提高代码的模块化、可维护性和安全性,降低耦合度。

    B.2.3 继承 (Inheritance)

    概念: 允许一个类 (派生类/子类) 继承另一个类 (基类/父类) 的属性和行为。
    继承方式 (Inheritance Types):
    public 继承: 基类的 publicprotected 成员在派生类中保持原访问级别。
    protected 继承: 基类的 publicprotected 成员在派生类中变为 protected
    private 继承: 基类的 publicprotected 成员在派生类中变为 private。 (通常不常用)
    派生类构造函数: 派生类的构造函数需要负责调用基类的构造函数,初始化基类部分。
    override 关键字: 用于显式声明派生类中的成员函数重写 (override) 了基类的虚函数 (virtual function)。
    final 关键字: 用于阻止类被继承或虚函数被重写。

    B.2.4 多态 (Polymorphism)

    概念: “一个接口,多种实现”。允许使用基类类型的指针或引用来操作派生类对象,从而实现不同的行为。
    静态多态 (Static Polymorphism)/编译时多态 (Compile-time Polymorphism): 通过函数重载和模板实现。在编译时确定调用哪个函数。
    动态多态 (Dynamic Polymorphism)/运行时多态 (Run-time Polymorphism): 通过虚函数和继承实现。在运行时根据对象的实际类型确定调用哪个函数。
    虚函数 (Virtual Functions): 在基类中使用 virtual 关键字声明的函数。允许派生类重写这些函数,实现动态多态。
    纯虚函数 (Pure Virtual Functions): 在基类中声明为 virtual 并且没有定义的函数,用 = 0 结尾。包含纯虚函数的类是抽象类 (abstract class),不能直接实例化,只能被继承,并且派生类必须实现基类中的纯虚函数才能被实例化。
    抽象类 (Abstract Class): 包含至少一个纯虚函数的类。用于定义接口,强制派生类实现特定功能。

    B.3 内存管理 (Memory Management)

    B.3.1 栈内存 (Stack Memory) 与堆内存 (Heap Memory)

    栈内存 (Stack Memory): 由编译器自动分配和释放,用于存储局部变量、函数参数、函数调用信息等。生命周期由作用域决定,速度快但空间有限。
    堆内存 (Heap Memory): 由程序员手动分配和释放,使用 new 运算符分配,使用 delete 运算符释放。生命周期由程序员控制,空间较大但需要手动管理,容易出现内存泄漏 (memory leak) 和悬 dangling 指针 (dangling pointer) 等问题。

    B.3.2 动态内存分配 (Dynamic Memory Allocation) 与释放 (Deallocation)

    new 运算符: 在堆上分配内存,返回分配内存的首地址。
    ⚝ 分配单个对象: dataType *ptr = new dataType;
    ⚝ 分配数组: dataType *arrayPtr = new dataType[arraySize];
    delete 运算符: 释放 new 分配的内存。
    ⚝ 释放单个对象: delete ptr;
    ⚝ 释放数组: delete[] arrayPtr;
    智能指针 (Smart Pointers): C++11 引入的智能指针,用于自动管理动态分配的内存,防止内存泄漏。
    std::unique_ptr: 独占式智能指针,确保同一时间只有一个智能指针指向该对象。
    std::shared_ptr: 共享式智能指针,允许多个智能指针指向同一对象,通过引用计数 (reference counting) 管理对象生命周期。
    std::weak_ptr: 弱引用智能指针,不增加引用计数,用于解决 std::shared_ptr 循环引用 (circular reference) 问题。

    B.3.3 RAII (Resource Acquisition Is Initialization)

    概念: 资源获取即初始化。一种编程惯用法,将资源的生命周期与对象的生命周期绑定,在对象构造时获取资源,在对象析构时释放资源。
    应用: 智能指针是 RAII 的典型应用,文件句柄、互斥锁等资源管理也可以使用 RAII 技术。
    优势: 自动资源管理,避免资源泄漏,提高代码的健壮性和安全性。

    B.4 模板 (Templates) 与泛型编程 (Generic Programming)

    B.4.1 函数模板 (Function Templates)

    定义: 创建可以用于多种数据类型的函数。函数体保持不变,数据类型作为参数传递。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 template <typename T>
    2 T max(T a, T b) {
    3 return (a > b) ? a : b;
    4 }

    实例化 (Instantiation): 编译器根据函数调用时使用的实际数据类型生成具体的函数代码。例如 max<int>(10, 20); 会生成 int max(int a, int b) 的函数。

    B.4.2 类模板 (Class Templates)

    定义: 创建可以用于多种数据类型的类。类的成员变量和成员函数可以使用模板参数表示数据类型。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 template <typename T>
    2 class MyVector {
    3 private:
    4 T *data;
    5 int size;
    6 public:
    7 // ...
    8 };

    实例化 (Instantiation): 使用类模板创建对象时,需要指定具体的数据类型。例如 MyVector<int> intVector;

    B.4.3 泛型算法 (Generic Algorithms)

    概念: 使用迭代器 (iterator) 操作各种容器 (container) 中的元素,算法与具体数据类型和容器类型解耦。
    STL 算法: C++ 标准模板库 (STL) 提供了大量的泛型算法,例如排序 (sort), 查找 (find), 复制 (copy), 转换 (transform) 等。

    B.5 异常处理 (Exception Handling)

    B.5.1 异常 (Exception) 的概念

    定义: 程序运行时发生的错误或异常情况,例如除零错误、内存分配失败、文件打开失败等。
    异常处理机制: C++ 提供了 try-catch 块来捕获和处理异常,使程序能够更健壮地处理错误情况。

    B.5.2 try-catch

    try: 包含可能抛出异常的代码。
    catch: 紧跟在 try 块后面,用于捕获和处理特定类型的异常。可以有多个 catch 块,分别处理不同类型的异常。

    1.双击鼠标左键复制此行;2.单击复制所有代码。
                                    
                                        
    1 try {
    2 // 可能抛出异常的代码
    3 // ...
    4 if (errorCondition) {
    5 throw exceptionObject; // 抛出异常
    6 }
    7 } catch (ExceptionType1 &e) {
    8 // 处理 ExceptionType1 类型的异常
    9 // ...
    10 } catch (ExceptionType2 &e) {
    11 // 处理 ExceptionType2 类型的异常
    12 // ...
    13 } catch (...) { // 捕获所有其他类型的异常 (catch-all)
    14 // 处理其他类型的异常
    15 // ...
    16 }

    throw 关键字: 用于抛出异常。
    异常类型 (Exception Types): 可以是基本数据类型、字符串、类对象等。通常使用继承自 std::exception 或其派生类的自定义异常类。
    栈展开 (Stack Unwinding): 当异常抛出后,程序会沿着函数调用栈向上查找匹配的 catch 块,期间栈上的局部对象会被自动析构 (通过 RAII 机制释放资源)。

    B.5.3 标准异常类 (Standard Exception Classes)

    std::exception: 所有标准异常类的基类。
    std::runtime_error: 运行时错误异常基类。
    std::logic_error: 逻辑错误异常基类。
    std::bad_alloc: 内存分配失败异常。
    std::out_of_range: 范围错误异常。
    std::invalid_argument: 无效参数异常。

    B.6 标准模板库 (Standard Template Library, STL) 简介

    B.6.1 容器 (Containers)

    序列容器 (Sequence Containers): 元素按线性顺序排列。
    std::vector: 动态数组,支持快速随机访问,尾部插入/删除效率高。
    std::deque (double-ended queue): 双端队列,支持快速随机访问,头部和尾部插入/删除效率高。
    std::list: 双向链表,不支持随机访问,任意位置插入/删除效率高。
    std::forward_list: 单向链表 (C++11),不支持随机访问,任意位置插入/删除效率高,空间效率更高。
    std::array (C++11): 固定大小数组,大小在编译时确定,与内置数组类似,但更安全。
    关联容器 (Associative Containers): 元素按键 (key) 排序,支持快速查找。
    std::set: 集合,存储唯一键,按键排序。
    std::multiset: 多重集合,允许存储重复键,按键排序。
    std::map: 映射,存储键值对 (key-value pairs),按键排序,键唯一。
    std::multimap: 多重映射,存储键值对,允许重复键,按键排序。
    无序关联容器 (Unordered Associative Containers) (C++11): 基于哈希表实现,元素无序,但查找效率更高 (平均情况下)。
    std::unordered_set: 无序集合,存储唯一键。
    std::unordered_multiset: 无序多重集合,允许存储重复键。
    std::unordered_map: 无序映射,存储键值对,键唯一。
    std::unordered_multimap: 无序多重映射,存储键值对,允许重复键。
    容器适配器 (Container Adapters): 基于现有容器实现的特殊容器。
    std::stack: 栈,后进先出 (LIFO)。
    std::queue: 队列,先进先出 (FIFO)。
    std::priority_queue: 优先级队列,元素按优先级排序。

    B.6.2 迭代器 (Iterators)

    概念: 类似于指针,用于遍历容器中的元素。提供统一的访问容器元素的方式,使算法可以独立于容器类型。
    迭代器类型: 输入迭代器 (input iterator), 输出迭代器 (output iterator), 前向迭代器 (forward iterator), 双向迭代器 (bidirectional iterator), 随机访问迭代器 (random access iterator)。不同类型的迭代器支持的操作不同。
    常用迭代器操作: * (解引用), ++ (自增), -- (自减,双向迭代器和随机访问迭代器), ==, !=, +, -, [] (随机访问迭代器)。
    容器的迭代器: 每个 STL 容器都提供了 begin()end() 函数返回迭代器,begin() 指向容器的第一个元素,end() 指向容器的末尾的下一个位置 (past-the-end)。

    B.6.3 算法 (Algorithms)

    概念: STL 提供的通用算法,用于操作容器中的元素。通过迭代器与容器交互,实现算法与容器的解耦。
    常用算法:
    ⚝ 排序算法: std::sort, std::stable_sort, std::partial_sort 等。
    ⚝ 查找算法: std::find, std::binary_search, std::count 等。
    ⚝ 复制算法: std::copy, std::move
    ⚝ 转换算法: std::transform
    ⚝ 删除算法: std::remove, std::unique
    ⚝ 数值算法: std::accumulate, std::inner_product
    ⚝ 生成算法: std::generate, std::iota
    ⚝ 关系算法: std::equal, std::mismatch
    ⚝ 堆算法: std::make_heap, std::push_heap, std::pop_heap, std::sort_heap
    算法的使用: 大多数 STL 算法接受迭代器作为参数,指定操作的元素范围。例如 std::sort(vector.begin(), vector.end());vector 容器中的所有元素进行排序。

    B.6.4 函数对象 (Function Objects)/仿函数 (Functors)

    概念: 行为类似函数的对象,即可以像函数一样被调用,但本质上是类对象。通过重载函数调用运算符 operator() 实现。
    用途: 可以作为算法的参数,自定义算法的行为。比普通函数指针更灵活,可以携带状态。
    STL 预定义函数对象: STL 提供了许多预定义的函数对象,例如算术运算 (plus, minus, multiplies, divides, modulus, negate), 比较运算 (equal_to, not_equal_to, greater, less, greater_equal, less_equal), 逻辑运算 (logical_and, logical_or, logical_not)。
    Lambda 表达式 (Lambda Expressions) (C++11): 一种更简洁的定义函数对象的方式,可以在需要函数对象的地方直接定义匿名函数。例如 std::sort(vector.begin(), vector.end(), [](int a, int b){ return a > b; }); 使用 Lambda 表达式定义降序排序的比较函数对象。

    本附录回顾了 C++ 编程的基础知识,这些知识是学习和应用 Qt 框架进行 GUI 编程的重要基石。希望读者通过本附录的复习,能够更好地理解本书后续章节的内容,并顺利掌握 C++ GUI 编程技能。

    Appendix C: 附录 C:参考文献与推荐阅读

    Appendix C1: C++ 编程基础

    书籍
    ▮▮▮▮ⓑ 《C++ Primer Plus (第6版)》中文版:Stephen Prata 著。C++ 入门经典教程,内容全面,讲解细致,适合初学者系统学习 C++ 语言。
    ▮▮▮▮ⓒ 《Effective C++:改善程序与设计的55个具体做法 (第3版)》中文版:Scott Meyers 著。C++ 进阶必读,深入剖析 C++ 编程中的常见问题和最佳实践。
    ▮▮▮▮ⓓ 《More Effective C++:35个改善编程与设计的有效方法》中文版:Scott Meyers 著。《Effective C++》的续作,进一步探讨 C++ 高级主题和技巧。
    ▮▮▮▮ⓔ 《Effective Modern C++:改善C++11/14编程的42个具体做法》中文版:Scott Meyers 著。关注现代 C++ (C++11/14) 的特性和最佳实践,帮助读者掌握现代 C++ 编程技巧。
    ▮▮▮▮ⓕ 《Effective STL:STL使用经验谈》中文版:Scott Meyers 著。深入讲解标准模板库 (STL) 的使用技巧和注意事项,提高 STL 编程效率。
    ▮▮▮▮ⓖ 《C++ Templates: The Complete Guide (第2版)》:David Vandevoorde, Nicolai M. Josuttis, Douglas Gregor 著。C++ 模板编程权威指南,深入剖析模板原理和应用。
    ▮▮▮▮ⓗ 《深入探索C++对象模型》中文版:Stanley B. Lippman 著。从底层剖析 C++ 对象模型的实现机制,帮助读者深入理解 C++ 语言特性。

    在线资源
    ▮▮▮▮ⓑ cppreference.com:权威的 C++ 语言和标准库在线文档,是学习和查阅 C++ 语法的必备网站。 https://zh.cppreference.com/
    ▮▮▮▮ⓒ C++ Core Guidelines:C++ 核心指南,由 Bjarne Stroustrup 等专家编写,旨在提供现代 C++ 编程的最佳实践和指导原则。 https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines.html
    ▮▮▮▮ⓓ Learn C++:一个免费的在线 C++ 教程,适合初学者入门。 https://www.learncpp.com/

    Appendix C2: Qt 框架编程

    书籍
    ▮▮▮▮ⓑ 《Qt Creator快速入门 (第4版)》:霍亚飞 著。中文 Qt 入门教程,基于 Qt Creator IDE,通过实例引导读者快速掌握 Qt 编程。
    ▮▮▮▮ⓒ 《C++ GUI Programming with Qt 4 (第2版)》:Jasmin Blanchette, Mark Summerfield 著。英文 Qt 4 经典教程,全面深入地讲解 Qt 4 框架的各个方面。虽然基于 Qt 4,但很多核心概念仍然适用。
    ▮▮▮▮ⓓ 《Advanced Qt Programming: Creating Efficient C++ Applications》:Mark Summerfield 著。英文 Qt 进阶教程,深入探讨 Qt 高级主题和性能优化技巧。
    ▮▮▮▮ⓔ 《Mastering Qt 5》:Guillaume Lazar, Juergen Bocklage-Ryannel, Robin Penea 著。英文 Qt 5 实战教程,通过丰富的示例项目,帮助读者掌握 Qt 5 的实际应用开发。
    ▮▮▮▮ⓕ 《Qt 6 Core and GUI Programming with C++》: Lee Ghin Lon 著。英文 Qt 6 最新教程,涵盖 Qt 6 的核心和 GUI 编程,适合学习最新 Qt 技术的读者。

    官方文档与在线资源
    ▮▮▮▮ⓑ Qt 官方文档 (Qt Documentation):Qt 官方提供的最权威、最全面的文档,包括类参考、教程、示例代码等。 https://doc.qt.io/
    ▮▮▮▮ⓒ Qt Wiki:Qt 社区维护的 Wiki 站点,包含大量的 Qt 技巧、教程和经验分享。 https://wiki.qt.io/
    ▮▮▮▮ⓓ Qt 示例 (Qt Examples):Qt SDK (软件开发工具包) 中自带的示例代码,涵盖 Qt 框架的各个模块和功能,是学习 Qt 编程的重要资源。
    ▮▮▮▮ⓔ Qt Blog:Qt 官方博客,发布 Qt 最新动态、技术文章和开发经验。 https://www.qt.io/blog
    ▮▮▮▮ⓕ Stack Overflow (Stack Overflow) 上的 Qt 标签:在 Stack Overflow 网站上搜索和提问 Qt 相关问题,可以获取社区的帮助和解答。 https://stackoverflow.com/questions/tagged/qt
    ▮▮▮▮ⓖ Qt Forum (Qt 论坛):Qt 官方论坛,用于讨论 Qt 相关技术问题和交流经验。 https://forum.qt.io/

    Appendix C3: 图形用户界面 (GUI) 编程理论与实践

    书籍
    ▮▮▮▮ⓑ 《用户界面设计模式》中文版:Tidwell, Jenifer 著。介绍常用的用户界面设计模式,帮助开发者设计用户友好的界面。
    ▮▮▮▮ⓒ 《About Face 4: The Essentials of Interaction Design》:Alan Cooper, Robert Reimann, David Cronin, Christopher Noessel 著。交互设计经典著作,深入探讨交互设计原则和方法。
    ▮▮▮▮ⓓ 《Don't Make Me Think, Revisited: A Common Sense Approach to Web Usability (3rd Edition)》:Steve Krug 著。Web 可用性经典著作,也适用于桌面 GUI 设计,强调用户体验和易用性。
    ▮▮▮▮ⓔ 《The Design of Everyday Things》:Donald A. Norman 著。设计心理学经典著作,从认知心理学角度分析日常物品的设计,对 GUI 设计也具有启发意义。

    在线资源
    ▮▮▮▮ⓑ Material Design (材料设计):Google 提出的 Material Design 设计规范,提供了一套现代、简洁、一致的 UI 设计语言。 https://material.io/design
    ▮▮▮▮ⓒ Apple Human Interface Guidelines (苹果人机界面指南):Apple 官方提供的 macOS 和 iOS 平台 UI 设计指南,遵循 Apple 平台的设计规范可以提升用户体验。 https://developer.apple.com/design/human-interface-guidelines/
    ▮▮▮▮ⓓ Microsoft Fluent Design System (微软 Fluent 设计系统):Microsoft 提出的 Fluent Design System 设计规范,用于 Windows 平台的 UI 设计。 https://learn.microsoft.com/en-us/windows/apps/design/fluent/

    Appendix C4: 现代 C++ 与 Qt 进阶主题

    书籍
    ▮▮▮▮ⓑ 《Modern C++ Design: Generic Programming and Design Patterns Applied》:Andrei Alexandrescu 著。探讨现代 C++ 设计模式和泛型编程技巧。
    ▮▮▮▮ⓒ 《C++ Concurrency in Action (第2版)》:Anthony Williams 著。深入讲解 C++ 并发编程,包括线程、锁、原子操作、并发数据结构等。
    ▮▮▮▮ⓓ 《Thinking in C++ (第2版)》 (Volumes 1 & 2):Bruce Eckel 著。C++ 经典入门和进阶教程,以实践为主导,深入浅出地讲解 C++ 编程思想。

    在线资源
    ▮▮▮▮ⓑ Meeting C++ Blog:德国 C++ 专家 Jens Weller 的博客,关注 C++ 最新动态、会议信息和技术文章。 https://meetingcpp.com/blog/
    ▮▮▮▮ⓒ Qt Quarterly:Qt 官方发布的季刊,包含 Qt 技术文章、案例分析和最佳实践。 https://www.qt.io/qt-quarterly
    ▮▮▮▮ⓓ KDAB Blog:KDAB 是一家专注于 Qt 和 C++ 的咨询公司,其博客发布了很多高质量的 Qt 和 C++ 技术文章。 https://www.kdab.com/blog/

    提示: 📚 表示书籍, 🌐 表示在线资源, 💻 表示实践资源。建议读者结合书籍、文档和实践,系统学习 C++ GUI 编程与 Qt 框架。同时,关注 Qt 社区和技术博客,及时了解最新技术动态。

    Appendix D: 附录 D:术语表

    Appendix D:术语表 (Glossary)

    本附录旨在为读者提供本书中常用术语的定义和解释,以便更好地理解 C++ GUI 编程和 Qt 框架的相关概念。术语按照英文首字母顺序排列。

    API (Application Programming Interface) (应用程序编程接口)

    ▮ 定义:一组定义、协议和工具,用于构建应用程序软件。API 规定了软件组件之间应该如何交互。在 Qt 框架中,API 指的是 Qt 库提供的各种类、函数、宏和信号槽机制,开发者通过调用这些 API 来实现 GUI 应用程序的功能。

    ▮ 示例:QWidget, QPushButton, connect() 等都是 Qt API 的一部分。

    信号 (Signal)

    ▮ 定义:在 Qt 框架中,信号是一种特殊类型的成员函数,由对象在状态改变或事件发生时发射 (emit)。信号本身不执行任何操作,它的作用是通知其他对象发生了特定事件。

    ▮ 解释:信号是 Qt 元对象系统 (Meta-Object System) 的核心组成部分,用于实现对象间的通信。当一个对象的内部状态发生改变,例如按钮被点击,它会发射一个信号。

    ▮ 关联术语:槽 (Slot), 信号与槽 (Signals and Slots)

    信号与槽 (Signals and Slots)

    ▮ 定义:Qt 框架中用于对象间通信的核心机制。信号由对象发射,当特定的事件发生时,槽是普通的成员函数,可以连接到信号。当信号发射时,与其连接的槽函数会被自动调用。

    ▮ 解释:信号与槽机制实现了对象之间的松耦合。发射信号的对象不需要知道哪个对象会接收到信号,也不需要知道接收对象是谁。接收对象只需要关注它感兴趣的信号,并在槽函数中处理接收到的信号。

    ▮ 优势:类型安全,松耦合,灵活性高。

    槽 (Slot)

    ▮ 定义:在 Qt 框架中,槽是普通的成员函数,可以被连接到信号。当信号发射时,与其连接的槽函数会被自动执行,用于响应信号所代表的事件。

    ▮ 解释:槽函数是信号的接收者,它定义了当接收到信号时应该执行的操作。槽函数可以是任何普通的成员函数,只需要在声明时标记为 slots 即可 (使用 Q_SLOTS 宏或 slots: 关键字)。

    ▮ 关联术语:信号 (Signal), 信号与槽 (Signals and Slots)

    CMake (Cross-platform Make) (跨平台构建工具)

    ▮ 定义:一个开源的、跨平台的自动化构建系统。CMake 使用简单的平台和编译器独立的配置文件来控制软件构建过程,并生成标准的构建文件 (如 Unix 的 Makefile 或 Windows 的 Visual Studio 工程)。

    ▮ 解释:CMake 是 Qt 官方推荐的构建工具之一,用于管理 Qt 项目的构建过程,包括编译、链接、生成可执行文件等。它比 qmake 更通用,可以用于非 Qt 项目的构建。

    ▮ 关联术语:qmake, 构建工具 (Build Tool)

    CLI (Command-Line Interface) (命令行界面)

    ▮ 定义:一种用户界面,用户通过输入文本命令来与计算机程序进行交互。与 GUI (图形用户界面) 相对。

    ▮ 解释:CLI 通常用于服务器管理、脚本编写和自动化任务等,而 GUI 更适用于桌面应用程序,提供更直观的用户交互体验。

    ▮ 关联术语:GUI (Graphical User Interface)

    控件 (Widget)

    ▮ 定义:GUI (图形用户界面) 中的基本构建块,用户可以直接与之交互的界面元素,例如按钮、标签、文本框、复选框等。

    ▮ 解释:在 Qt Widgets 模块中,控件是 QWidget 类的子类。Qt 提供了丰富的内置控件,开发者也可以自定义控件来满足特定的界面需求。

    ▮ 示例:QPushButton (按钮), QLabel (标签), QLineEdit (文本框)

    跨平台 (Cross-platform)

    ▮ 定义:指软件或应用程序能够在多种不同的操作系统或硬件平台上运行的能力,而无需为每个平台单独编写代码或进行重大修改。

    ▮ 解释:Qt 框架的核心优势之一是跨平台性。使用 Qt 开发的 GUI 应用程序可以很容易地在 Windows, macOS, Linux 以及移动平台 (Android, iOS) 等多种平台上编译和运行。

    ▮ 优势:代码复用,降低开发成本,提高开发效率。

    委托 (Delegate)

    ▮ 定义:在 Qt 模型/视图架构 (Model/View Architecture) 中,委托负责控制视图 (View) 中数据的显示和编辑方式。委托将数据渲染到视图中,并处理用户的编辑请求。

    ▮ 解释:委托实现了视图和模型之间的解耦,使得可以灵活地定制数据的显示和编辑行为。Qt 提供了默认的委托,开发者也可以自定义委托来满足特定的显示和编辑需求。

    ▮ 关联术语:模型/视图架构 (Model/View Architecture), 模型 (Model), 视图 (View)

    动态部署 (Dynamic Deployment)

    ▮ 定义:一种应用程序部署方式,应用程序依赖的库 (如 Qt 库) 以动态链接库 (DLLs 或 Shared Libraries) 的形式存在,在运行时加载。

    ▮ 解释:动态部署的应用程序体积较小,但需要确保目标系统上存在应用程序所依赖的动态库。在 Windows 平台,通常需要携带 Qt 的 DLLs;在 macOS 平台,需要携带 Qt Frameworks;在 Linux 平台,需要确保 Qt 共享库在系统库路径中。

    ▮ 关联术语:静态部署 (Static Deployment)

    事件 (Event)

    ▮ 定义:在 GUI 编程中,事件是指用户与程序交互时发生的动作,或者系统内部发生的状态变化,例如鼠标点击、键盘按键、窗口大小改变、定时器超时等。

    ▮ 解释:GUI 应用程序是事件驱动的。Qt 框架提供了完善的事件处理机制,用于捕获和响应各种事件。

    ▮ 示例:鼠标事件 (Mouse Events), 键盘事件 (Key Events), 窗口事件 (Window Events)

    事件过滤器 (Event Filter)

    ▮ 定义:Qt 事件处理机制中的一种高级特性,允许对象拦截和预处理发送到其他对象的事件。事件过滤器可以在事件到达目标对象之前对其进行处理,甚至可以阻止事件继续传递。

    ▮ 解释:事件过滤器可以用于实现全局的事件监听和处理,例如全局快捷键、输入验证等。

    事件循环 (Event Loop)

    ▮ 定义:GUI 应用程序的核心机制,负责不断地监听和处理发生的事件。事件循环从系统事件队列中取出事件,并将事件分发给相应的对象进行处理。

    ▮ 解释:Qt 应用程序的 main() 函数中通常会创建一个 QApplication 对象,并调用 exec() 函数启动事件循环。事件循环持续运行,直到应用程序退出。

    框架 (Framework)

    ▮ 定义:一个为解决特定领域问题而设计的软件结构,提供了一组预定义的类、接口、函数和工具,用于构建应用程序。框架通常定义了应用程序的基本架构和流程,开发者只需要关注业务逻辑的实现。

    ▮ 解释:Qt 是一个 C++ GUI 应用程序框架,提供了丰富的库和工具,用于快速开发跨平台的 GUI 应用程序。

    ▮ 示例:Qt (Qt Framework), MFC (Microsoft Foundation Classes)

    GUI (Graphical User Interface) (图形用户界面)

    ▮ 定义:一种用户界面,用户通过图形化的方式 (如窗口、菜单、按钮、图标等) 与计算机程序进行交互,相对于 CLI (命令行界面)。

    ▮ 解释:GUI 提供了更直观、友好的用户交互体验,广泛应用于桌面应用程序、移动应用程序和嵌入式系统等。

    ▮ 关联术语:CLI (Command-Line Interface)

    IDE (Integrated Development Environment) (集成开发环境)

    ▮ 定义:一种软件应用程序,为程序员提供开发软件所需的各种工具,通常包括代码编辑器、编译器、调试器、构建工具等。

    ▮ 解释:Qt Creator 是 Qt 官方提供的 IDE,专门用于 Qt 应用程序的开发,集成了代码编辑、UI 设计、编译构建、调试测试等功能。

    ▮ 示例:Qt Creator, Visual Studio, Xcode, Eclipse

    布局管理器 (Layout Manager)

    ▮ 定义:GUI 框架中用于自动管理控件 (Widget) 布局的组件。布局管理器根据一定的规则和策略,自动调整控件的大小和位置,以适应窗口大小的变化和不同的屏幕分辨率。

    ▮ 解释:Qt 提供了多种布局管理器,如 QHBoxLayout (水平布局), QVBoxLayout (垂直布局), QGridLayout (网格布局) 等,用于实现灵活的界面布局。

    MOC (Meta-Object Compiler) (元对象编译器)

    ▮ 定义:Qt 元对象系统 (Meta-Object System) 的关键组成部分,是 Qt 特有的预处理器。MOC 扫描 C++ 源代码,解析包含 Q_OBJECT 宏的类,并生成额外的 C++ 代码,实现信号与槽、反射 (Reflection) 等元对象特性。

    ▮ 解释:MOC 是 Qt 框架实现信号与槽机制的基础。在编译 Qt 项目时,MOC 会自动运行,处理包含元对象特性的类。

    ▮ 关联术语:元对象系统 (Meta-Object System), QObject

    模型 (Model)

    ▮ 定义:在 Qt 模型/视图架构 (Model/View Architecture) 中,模型负责存储和管理应用程序的数据。模型提供接口,供视图 (View) 和委托 (Delegate) 访问和操作数据。

    ▮ 解释:模型实现了数据和界面的分离,使得可以方便地更换不同的数据源或界面显示方式。Qt 提供了多种内置模型类,如 QStringListModel, QStandardItemModel, QSqlTableModel 等,开发者也可以自定义模型。

    ▮ 关联术语:模型/视图架构 (Model/View Architecture), 视图 (View), 委托 (Delegate)

    模型/视图架构 (Model/View Architecture)

    ▮ 定义:一种 GUI 应用程序的设计模式,用于实现数据与界面的分离。模型/视图架构将应用程序分为三个核心组件:模型 (Model)、视图 (View) 和委托 (Delegate)。

    ▮ 优势:数据与界面分离,代码复用,灵活性高,可维护性强。

    ▮ 关联术语:模型 (Model), 视图 (View), 委托 (Delegate)

    对象模型 (Object Model)

    ▮ 定义:Qt 的对象模型是 Qt 框架的基础,它基于 C++,并扩展了标准 C++ 的对象概念,提供了对象树 (Object Tree)、信号与槽 (Signals and Slots)、属性系统 (Property System) 等特性。

    ▮ 解释:Qt 的对象模型使得 Qt 框架具有高度的灵活性和可扩展性,是构建复杂 GUI 应用程序的基础。

    对象树 (Object Tree)

    ▮ 定义:Qt 对象模型的核心概念,Qt 对象可以组织成树状结构,形成父子关系。当父对象被销毁时,其所有子对象也会被自动销毁。

    ▮ 解释:对象树简化了 Qt 应用程序的内存管理。Qt 对象的内存管理通常是自动的,开发者无需手动删除通过父子关系创建的对象。

    QML (Qt Meta-Object Language)

    ▮ 定义:Qt 框架中用于声明式 UI (User Interface) 开发的标记语言。QML 使用 JavaScript 作为其表达式语言,可以方便地描述用户界面和用户交互。

    ▮ 解释:QML 通常与 Qt Quick 模块一起使用,用于构建现代、流畅、动画丰富的用户界面。QML 前端可以与 C++ 后端结合,实现复杂的应用程序逻辑。

    ▮ 关联术语:Qt Quick

    QObject

    ▮ 定义:Qt 对象模型的基础类,是 Qt 中所有支持元对象特性 (如信号与槽、属性系统等) 的类的基类。所有使用信号与槽机制的类都必须直接或间接地继承自 QObject

    ▮ 解释:QObject 类提供了元对象系统所需的基础设施,包括元对象信息、信号与槽机制、属性系统、对象树等。

    ▮ 关联术语:元对象系统 (Meta-Object System), MOC (元对象编译器)

    qmake

    ▮ 定义:Qt 官方提供的传统的构建工具,用于管理 Qt 项目的构建过程。qmake 使用 .pro 文件作为项目配置文件,并生成 Makefile 或 Visual Studio 工程文件。

    ▮ 解释:qmake 是 Qt 早期版本的主要构建工具,但在现代 Qt 开发中,CMake 逐渐成为更推荐的选择。

    ▮ 关联术语:CMake (Cross-platform Make), 构建工具 (Build Tool)

    Qt Creator

    ▮ 定义:Qt 官方提供的跨平台 IDE (集成开发环境),专门用于 Qt 应用程序的开发。Qt Creator 集成了代码编辑器、UI 设计器 (Qt Designer)、调试器、构建工具等,提供了 Qt 开发所需的所有工具。

    ▮ 解释:Qt Creator 是 Qt 开发的首选 IDE,提供了良好的 Qt 支持和开发体验。

    ▮ 关联术语:IDE (Integrated Development Environment)

    Qt Designer

    ▮ 定义:Qt Creator 集成的一个可视化 UI 设计器,用于创建和编辑 Qt Widgets 模块的用户界面。Qt Designer 允许开发者通过拖拽控件的方式,快速设计 GUI 界面,并生成 .ui 界面描述文件。

    ▮ 解释:Qt Designer 简化了 GUI 界面的设计过程,提高了开发效率。

    Qt Quick

    ▮ 定义:Qt 框架中用于构建现代、流畅、动画丰富的用户界面的模块。Qt Quick 基于 QML 语言和 JavaScript,并利用硬件加速进行图形渲染。

    ▮ 解释:Qt Quick 适用于开发具有现代 UI 风格的应用程序,例如移动应用程序、嵌入式设备界面等。

    ▮ 关联术语:QML (Qt Meta-Object Language)

    Qt Widgets

    ▮ 定义:Qt 框架中提供经典控件 (Widgets) 的模块,用于构建传统的桌面风格 GUI 应用程序。Qt Widgets 基于 C++,提供了丰富的控件和布局管理器。

    ▮ 解释:Qt Widgets 模块是 Qt 框架的传统 GUI 模块,适用于开发需要原生桌面风格界面的应用程序。

    反射 (Reflection)

    ▮ 定义:程序在运行时检查自身结构 (如类、对象、属性、方法等) 的能力。在 Qt 的元对象系统 (Meta-Object System) 中,反射机制允许在运行时获取对象的信息,例如类名、属性列表、信号和槽列表等。

    ▮ 解释:反射机制是 Qt 元对象系统的重要特性,为信号与槽、属性系统、动态属性等功能提供了基础。

    ▮ 关联术语:元对象系统 (Meta-Object System)

    SDK (Software Development Kit) (软件开发工具包)

    ▮ 定义:一组软件开发工具和文档,用于开发特定平台或系统的应用程序。Qt SDK 包含了 Qt 库、Qt Creator IDE、Qt 工具链 (如 qmake, CMake)、Qt 文档和示例代码等,提供了 Qt 开发所需的完整环境。

    ▮ 解释:安装 Qt SDK 可以快速搭建 Qt 开发环境,开始 Qt 应用程序的开发。

    静态部署 (Static Deployment)

    ▮ 定义:一种应用程序部署方式,应用程序依赖的库 (如 Qt 库) 被静态链接到可执行文件中。

    ▮ 解释:静态部署的应用程序是自包含的,不需要依赖外部的库文件,可以独立运行。但静态链接会导致可执行文件体积较大,并且更新 Qt 库需要重新编译和发布整个应用程序。

    ▮ 关联术语:动态部署 (Dynamic Deployment)

    样式表 (Style Sheets)

    ▮ 定义:Qt 框架中用于自定义控件 (Widget) 外观和风格的机制,类似于 CSS (Cascading Style Sheets) 用于网页样式设计。Qt 样式表允许开发者通过类似 CSS 的语法,设置控件的颜色、字体、背景、边框等属性,实现界面的美化和风格统一。

    ▮ 解释:Qt 样式表提供了灵活的界面定制方式,可以方便地修改应用程序的整体风格,或者为特定的控件设置不同的样式。

    主题 (Theme)

    ▮ 定义:一组预定义的界面风格和样式,用于统一应用程序的外观。Qt 框架支持使用不同的主题,以改变应用程序的整体风格。

    ▮ 解释:Qt 提供了多种内置主题,例如 Fusion, Windows, macOS, GTK+ 等,开发者也可以自定义主题。

    UI (User Interface) (用户界面)

    ▮ 定义:用户与计算机程序或设备进行交互的界面,包括 GUI (图形用户界面) 和 CLI (命令行界面) 等类型。

    ▮ 解释:用户界面设计 (UI Design) 是软件开发的重要组成部分,良好的用户界面可以提高用户体验和应用程序的易用性。

    视图 (View)

    ▮ 定义:在 Qt 模型/视图架构 (Model/View Architecture) 中,视图负责将模型 (Model) 中的数据以可视化的方式呈现给用户。视图从模型中获取数据,并将其显示在屏幕上。

    ▮ 解释:Qt 提供了多种内置视图类,如 QListView, QTableView, QTreeView 等,用于显示不同类型的数据。开发者也可以自定义视图。

    ▮ 关联术语:模型/视图架构 (Model/View Architecture), 模型 (Model), 委托 (Delegate)

    QWidget

    ▮ 定义:Qt Widgets 模块中所有控件 (Widget) 的基类。QWidget 类提供了所有控件的通用功能,例如窗口属性、事件处理、绘图、布局管理等。

    ▮ 解释:几乎所有的 GUI 控件都直接或间接地继承自 QWidget