Эх сурвалжийг харах

ingest: 05-Qt应用开发(9篇)

OpenCode 11 цаг өмнө
parent
commit
dd512b9b7f

+ 899 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/01-C++基础与面向对象.md

@@ -0,0 +1,899 @@
+---
+title: C++基础与面向对象
+tags: [嵌入式Linux, Qt, C++, 面向对象, 类与对象, 构造析构, this指针, 继承, 重载, 多态, 虚函数, 封装, 抽象类, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第一章 在Ubuntu下编写C++、第二章 C++基础;例程 Embedded-Qt-Tutorial/C++/01_hello_world~11_abstract_class"
+---
+
+# C++基础与面向对象
+
+> 💡 **关联知识**:[[05-Qt应用开发/02-Qt入门与项目结构]](Qt 本身就是 C++ 库,`QMainWindow`、`QPushButton` 都建立在本文的继承与多态之上)、[[05-Qt应用开发/03-信号与槽]](`signals`/`slots`/`Q_OBJECT` 是 Qt 对 C++ 语法的扩展);延伸阅读:[[Linux+C+C++技术体系梳理/1. C语言基础/12. 结构体]](类可以理解为结构体的升级版本)
+
+Qt 是建立在 C++ 之上的。要写出能看懂、能修改的 Qt 程序,不需要成为 C++ 专家,但必须吃透 **类与对象、构造析构、this、继承、重载、多态、封装、抽象类** 这几块概念——Qt 的每一个类、每一次"重写父类函数"、每一个 `ui->setupUi(this)` 都用到了它们。
+
+本篇先交代 C++ 与 C 的差异、g++ 编译流程和命名空间,再以一条"狗类"主线把面向对象的核心概念逐个拆开,每个概念都配有 **完整可编译的源码**。读完即可独立写 C++ 小项目,也能顺畅进入 Qt。
+
+---
+
+## 1. C++ 简介与与 C 的区别
+
+### 1.1 C++ 是什么
+
+C++(c plus plus)是一种**静态类型的、编译式的、通用的、大小写敏感的、不规则的**编程语言,同时支持:
+
+- **过程化编程**(和 C 一样)
+- **面向对象编程**(类、继承、多态)
+- **泛型编程**(模板)
+
+C++ 由 Bjarne Stroustrup 于 1979 年在贝尔实验室开始设计,最初命名为"带类的 C",1983 年更名为 C++。C++ 是 C 的**超集**:任何合法的 C 程序都是合法的 C++ 程序。
+
+标准方面:C++98 是第一个标准,此后陆续有 C++03/11/14/17 等版本(本教程 `.pro` 中的 `CONFIG += c++11` 即启用 C++11)。**要记住版本原则:固定一个版本,不要随意升级**。
+
+学习 C++ 要**理解概念,而不是深究语言技术细节**。带着本篇的概念去学 Qt,遇到 `public`、`virtual`、重写、重载时就能对上号。
+
+### 1.2 C++ 与 C 的主要区别
+
+原书第二章开篇即说明:指针、数据类型、变量类型、判断、循环等知识在 C 与 C++ 中基本一样,**不再重复讲**;重点讲 C++ 的新特性和面向对象。差异可归纳如下:
+
+| 对比项 | C | C++ |
+| ------ | - | --- |
+| 布尔类型 | 旧标准无 `bool`,用 `int` 代替 | 原生 `bool`(`true` / `false`) |
+| 变量定义位置 | 传统上必须集中在程序(块)开头 | **随用随定义** |
+| 初始化方式 | `int x = 100;` | 还支持 `int x(100);` 直接初始化 |
+| 输入输出 | `scanf` / `printf` + 格式符 | `cin` / `cout` + `<<` `>>`,无需格式符 |
+| 命名空间 | 无 | 有 `namespace`,避免命名冲突 |
+| 字符串 | `char[]` + `<string.h>` | `std::string` 类型,方便好用 |
+| 编程范式 | 过程化 | 过程化 + 面向对象 + 泛型 |
+| 数据结构升级 | `struct` 只有数据 | `class` = 数据 + 成员函数 + 访问控制 |
+| 编译方式 | `gcc test.c -o test` | `g++ test.cpp -o test` |
+
+一句话:**C 是 C++ 的子集**,C++ 在保留 C 能力的同时,把"函数 + 数据"组织成了"类"。
+
+### 1.3 C++ 环境设置
+
+要编译 C++,需要 C 编译器 `gcc`(可选)和 C++ 编译器 `g++`。新装的 Ubuntu 默认没有:
+
+```bash
+# 安装 gcc 与 g++
+sudo apt-get install gcc g++
+
+# 安装其他常用库
+sudo apt-get install lsb-core lib32stdc++6
+
+# 查看版本,确认安装成功
+g++ -v
+gcc -v
+```
+
+若编译 Qt 时提示 `Cannot find -lGL`,还需补装 OpenGL:
+
+```bash
+sudo apt-get install libglu1-mesa-dev
+```
+
+### 1.4 第一个 C++ 程序
+
+在终端创建目录并编辑源文件:
+
+```bash
+mkdir C++                  # 创建一个 C++ 目录
+cd C++                     # 进入该目录
+mkdir 01_hello_world       # 创建 01_hello_world 目录
+cd 01_hello_world          # 进入该目录
+vi 01_hello_world.cpp      # 编辑 cpp 文件,拷贝下面的内容
+```
+
+完整源码:
+
+```cpp
+#include <iostream>
+using namespace std;
+
+int main()
+{
+    cout << "Hello, World!" << endl;
+    return 0;
+}
+```
+
+逐行解释:
+
+- 第 1 行 `#include <iostream>`:包含标准输入输出流头文件。注意**不能**写成 `iostream.h`——带 `.h` 的是非标准的 C 风格库,不带 `.h` 的是标准输入输出流,必须配合命名空间使用。
+- 第 2 行 `using namespace std;`:告诉编译器使用标准命名空间 `std`。`cin`、`cout`、`endl` 都属于 `std`,不写这句就得写 `std::cout`。
+- 第 3 行 `int main()`:主函数,程序入口。
+- 第 5 行 `cout << "Hello, World!" << endl;`:向屏幕输出并换行。`<<` 是**插入运算符**(被重载过),`endl` 是换行语句。
+- 第 6 行 `return 0;`:终止 `main()`,并把状态码返回给调用进程。
+
+编译与运行:
+
+```bash
+g++ 01_hello_world.cpp -o 01_hello_world   # -o 指定输出的可执行文件名
+./01_hello_world                           # 输出 Hello, World! 并换行
+```
+
+补充:`<<` 可以连续使用(如 `cout << "A\n" << "B" << endl;`),也可以直接用 C 的 `\n` 换行——这说明 **C++ 可以直接使用 C 语言语句**。
+
+### 1.5 C++ 新特性(2.1)
+
+原书只强调两个常用新特性:
+
+1. **布尔类型 `bool`**:C++ 比 C 新增的数据类型。新标准 C 语言也已支持 `bool`,但旧标准 C 没有,编译器无法解释。
+2. **变量可随用随定义、可直接初始化**:
+
+```cpp
+int x = 100;   // 传统写法
+int x(100);    // C++ 直接初始化,等价于 x = 100
+```
+
+### 1.6 C++ 的输入输出方式(2.2)
+
+C 语言用 `scanf` / `printf`,C++ 用 `cin` / `cout` 代替,输入输出流程不变,关键字和用法变了。
+
+```text
+C  :输入设备 → 输入流 scanf  → 变量 → printf   → 输出流 → 输出设备
+C++:输入设备 → 输入流 cin    → 变量 → cout     → 输出流 → 输出设备
+```
+
+效率上 `scanf` / `printf` 更高,但 `cin` / `cout` 更**方便**。
+
+`cout` 语法:
+
+```cpp
+cout << x << endl;     // x 可为任意数据类型,甚至表达式
+cout << x << y;        // endl 可省略,仅表示换行
+```
+
+- 正确:`cout << x << y;`(变量之间用 `<<`)
+- 错误:`cout << x, y << endl;`(变量之间**不能**用逗号)
+
+`cin` 语法:
+
+```cpp
+cin >> x;              // x 可为任意数据类型
+cin >> x >> y;         // 连续输入两个不同变量
+```
+
+### 1.7 命名空间 namespace(2.3)
+
+`using namespace std;` 我们已经用过。它的本质是**给名字加前缀,避免冲突**:
+
+- C++ 标准库定义了 `vector` 容器,你自己也写了个 `vector` 类,名字就冲突了。于是标准库把名字都放进 `std`,你要用就写 `std::vector`。
+- `::` 是**作用域运算符**,`std::cin` 读作"std 命名空间下的 cin"。
+- 老是写全名很繁琐,在没有冲突时就可以写一句 `using namespace std;` 偷懒。
+
+自定义命名空间示例(`02_namespace_example.cpp`):
+
+```cpp
+#include <iostream>
+using namespace std;
+
+namespace A
+{
+    int x = 1;
+    void fun() {
+        cout << "A namespace" << endl;
+    }
+}
+using namespace A;
+
+int main()
+{
+    fun();          // 声明 using namespace A 后可直接调用
+    A::x = 3;       // 修改 A 命名空间下的 x
+    cout << A::x << endl;
+    A::fun();       // 显式指定 A 命名空间下的 fun
+    return 0;
+}
+```
+
+逐行解释:
+
+- 第 4~10 行:自定义命名空间 `A`,内部有变量 `x`(初值 1)和函数 `fun()`。
+- 第 11 行:`using namespace A;` 声明使用命名空间 A。
+- 第 14 行:声明后才能直接写 `fun();`,否则必须写成 `A::fun();`。
+- 第 15 行:把 `A::x` 重新赋值为 3。
+- 第 16 行:打印 `A::x`。
+- 第 17 行:显式调用 `A::fun()`。
+
+
+初学 Qt 时较少看到命名空间,但 Qt 里同样可以自定义命名空间并正常使用。
+
+---
+
+## 2. C++ 面向对象
+
+面向对象的三大特征是 **继承、多态、封装**。下面以"狗"为主线,把每个概念都配一份例程源码。
+
+### 2.1 类和对象(2.2.1)
+
+- **类(class)**:C++ 的核心特性,通常称为"用户定义的类型"。类用于指定对象的形式,包含**数据表示法**和**处理数据的方法**,二者合称类的**成员**。
+- **对象(object)**:从类实例化出来的具体事物。
+
+类比:把"狗"抽象成一个类(有名字、年龄、会跑会叫),"旺财"就是对象。抽象时只挑出我们关心的几种属性即可。
+
+从类实例化对象有两种方式:
+
+| 方式 | 语法 | 内存位置 | 释放方式 |
+| ---- | ---- | -------- | -------- |
+| 栈中实例化 | `Dog dog1;` | 栈 | 离开作用域自动释放 |
+| 堆中实例化 | `Dog *dog2 = new Dog();` | 堆 | 必须 `delete dog2;` |
+
+完整源码(`03_class_dog_example.cpp`):
+
+```cpp
+#include <iostream>
+#include <string>
+using namespace std;
+
+class Dog
+{
+public:
+    string name;
+    int age;
+
+    void run() {
+        cout << "小狗的名字是:" << name << "," << "年龄是" << age << endl;
+    }
+};
+
+int main()
+{
+    Dog dog1;
+
+    dog1.name = "旺财";
+    dog1.age = 2;
+    dog1.run();
+
+    Dog *dog2 = new Dog();
+
+    if (NULL == dog2) {
+        return 0;
+    }
+    dog2->name = "富贵";
+    dog2->age = 1;
+    dog2->run();
+
+    delete dog2;
+    dog2 = NULL;
+    return 0;
+}
+```
+
+逐行解释:
+
+- 第 5 行:定义 `Dog` 类,类名尽量贴近它描述的事物,一看就懂。
+- 第 7 行:访问限定符 `public`(公有的)。此外还有 `private`(私有的)、`protected`(受保护的)。**不写访问限定符默认是 `private`**。简单理解:访问限定符就是给成员变量和成员函数设置访问权限。
+- 第 8~11 行:字符串变量 `name`、整型变量 `age`,以及方法 `run()`,在里面打印狗名和年龄。`string` 是 C++ 的数据类型,方便好用,使用频率很高。
+- 第 18 行:从栈中实例化对象 `dog1`。
+- 第 20~22 行:给 `dog1` 的成员赋值,并调用 `run()`。
+- 第 24 行:从堆中实例化对象,凡是用关键字 `new` 的都是从堆中实例化。
+- 第 26 行:堆对象需要开辟内存,指针指向那块内存;若 `new` 申请失败则指针为 `NULL`,程序直接退出,这体现了严谨性。
+- 第 29~31 行:给 `dog2` 成员赋值并调用 `run()`。
+- 第 34、35 行:释放内存,并将 `dog2` 重新指向 `NULL`。
+
+
+类与结构体的关系:**类可以说是结构体的升级版本**——结构体只有数据,类额外提供成员函数和访问控制。
+
+### 2.2 构造函数与析构函数(2.2.1.1)
+
+定义类时,如果没有自己写构造函数和析构函数,**编译器会自动生成**一个什么也不做的构造函数和析构函数,所以我们平时注意不到。
+
+**构造函数**:在对象实例化时被系统自动调用,**仅且调用一次**。特点:
+
+1. 构造函数必须与**类名同名**;
+2. 可以**重载**;
+3. **没有返回类型**,即使 `void` 也不行。
+
+**析构函数**:与构造函数相反,在对象结束其生命周期时系统自动执行。特点:
+
+1. 格式为 `~类名()`;
+2. 调用时释放内存(资源);
+3. `~类名()` **不能加参数**;
+4. **没有返回值**,即使是 `void` 也不行。
+
+完整源码(`04_structor_example.cpp`):
+
+```cpp
+#include <iostream>
+#include <string>
+using namespace std;
+
+class Dog
+{
+public:
+    Dog();
+    ~Dog();
+};
+
+int main()
+{
+    Dog dog;
+    cout << "构造与析构函数示例" << endl;
+    return 0;
+}
+
+Dog::Dog()
+{
+    cout << "构造函数执行!" << endl;
+}
+
+Dog::~Dog()
+{
+    cout << "析构函数执行!" << endl;
+}
+```
+
+逐行解释:
+
+- 第 5~10 行:定义狗类,并声明构造函数与析构函数。
+- 第 14 行:用 `Dog` 类实例化对象 `dog`。
+- 第 15 行:打印"构造与析构函数示例"。
+- 第 19~22 行:**类的函数可以在类里实现,也可以在类外实现**,在类外实现时需要用 `::`。这里把构造函数定义在类外,打印"构造函数执行!"。
+- 第 24~27 行:同样在类外定义析构函数,打印"析构函数执行!"。
+
+
+执行顺序可以预测:对象实例化时先调用构造函数(`Dog()` 先执行)→ 执行 `main()` 中的 `cout` → 对象生命周期结束时调用析构函数。
+
+### 2.3 this 指针(2.2.1.2)
+
+一个类中不同对象调用自己的成员函数时,其实调用的是**同一段函数代码**。那么成员函数如何知道要访问哪个对象的数据成员?答案就是 **this 指针**。
+
+- 每个对象都拥有一个 `this` 指针,`this` 记录对象的内存地址。
+- 在 C++ 中,`this` 指针是**指向类自身数据的指针**,简单说就是指向当前类的当前实例对象。
+
+特点:
+
+1. `this` 只能在**成员函数**中使用,全局函数、静态函数都不能使用。实际上成员函数默认第一个参数为 `T * const this`,即 `int func(int p)` 在编译器看来原型是 `int func(T * const this, int p)`。
+2. `this` 在成员函数开始前构造,在成员函数结束后清除。
+3. `this` 的存放位置因编译器而异:可能在栈、寄存器,甚至全局变量。
+
+完整源码(`05_this_pointer_example.cpp`):
+
+```cpp
+#include <iostream>
+#include <string>
+using namespace std;
+
+class Dog
+{
+public:
+    string name;
+    void func();
+};
+
+int main()
+{
+    Dog dog;
+    dog.func();
+    return 0;
+}
+
+void Dog::func()
+{
+    this->name = "旺财";
+    cout << "小狗的名字叫:" << this->name << endl;
+}
+```
+
+逐行解释:
+
+- 第 21、22 行:在成员函数里使用 `this` 指针指向类成员 `name`,先赋值为"旺财",再打印。程序输出"小狗的名字叫:旺财"。
+
+
+后面学 Qt 时会经常遇到 `this`,例如 `ui->setupUi(this)`、`new QPushButton(this)`——这里的 `this` 就是"当前窗口对象自身",作为父对象传给子控件。
+
+### 2.4 继承(2.2.2)
+
+继承允许我们**依据另一个类来定义一个类**,从而达到**重用代码、提高执行效率**的效果。
+
+- **基类(base class)**:已有的类。
+- **派生类(derived class)**:新建的类。
+
+Qt 里大量使用继承:当 Qt 的类不满足要求时,就继承它并重写相应函数。
+
+派生类定义形式:
+
+```cpp
+class derived-class : access-specifier base-class
+```
+
+`access-specifier` 是 `public`、`protected`、`private` 之一;**不使用则默认为 `private`**。继承方式:
+
+| 继承方式 | 基类 public 成员 | 基类 protected 成员 | 基类 private 成员 |
+| -------- | ---------------- | ------------------- | ----------------- |
+| public 公有继承 | 成为派生类 public | 成为派生类 protected | 不可直接访问(可通过基类 public/protected 成员间接访问) |
+| protected 保护继承 | 成为派生类 protected | 成为派生类 protected | 不可直接访问 |
+| private 私有继承 | 成为派生类 private | 成为派生类 private | 不可直接访问 |
+
+完整源码(`06_inherit_example.cpp`,公有继承):
+
+```cpp
+#include <iostream>
+#include <string>
+using namespace std;
+
+/*动物类,抽象出下面两种属性,
+ *颜色和体重,是每种动物都具有的属性
+ */
+class Animal
+{
+public:
+    /* 颜色成员变量 */
+    string color;
+    /* 体重成员变量 */
+    int weight;
+};
+
+/*让狗类继承这个动物类,并在狗类里写自己的属性。
+ *狗类拥有自己的属性name,age,run()方法,同时也继承了
+ *动物类的color 和weight 的属性
+ */
+class Dog : public Animal
+{
+public:
+    string name;
+    int age;
+    void run();
+};
+
+int main()
+{
+    Dog dog;
+    dog.name = "旺财";
+    dog.age = 2;
+    dog.color = "黑色";
+    dog.weight = 120;
+    cout << "狗的名字叫:" << dog.name << endl;
+    cout << "狗的年龄是:" << dog.age << endl;
+    cout << "狗的毛发颜色是:" << dog.color << endl;
+    cout << "狗的体重是:" << dog.weight << endl;
+    return 0;
+}
+```
+
+逐行解释:
+
+- 第 21 行:`Animal` 作为基类,`Dog` 作为派生类,`Dog` 公有继承了 `Animal`。
+- 第 31~39 行:`dog` 对象既使用了自身成员 `name`、`age`,又使用了从 `Animal` 继承来的 `color`、`weight`,证明继承生效。
+
+### 2.5 重载(2.2.3)
+
+C++ 允许在**同一作用域**中为某个函数和运算符指定多个定义,分别称为**函数重载**和**运算符重载**。
+
+- 重载声明:与之前已在该作用域声明过的函数/方法**同名**,但**参数列表和实现不同**。
+- 重载决策:调用时编译器把实参类型与各定义中的形参类型比较,选出最合适的定义。
+
+#### 2.5.1 函数重载(2.2.3.1)
+
+同一作用域内可声明几个功能类似的**同名函数**,但形参(个数、类型或顺序)必须不同。**不能仅靠返回类型不同来重载函数。**
+
+完整源码(`07_func_overloading.cpp`):
+
+```cpp
+#include <iostream>
+#include <string>
+using namespace std;
+
+class Dog
+{
+public:
+    string name;
+    void getWeight(int weight) {
+        cout << name << "的体重是:" << weight << "kG" << endl;
+    }
+
+    void getWeight(double weight) {
+        cout << name << "的体重是:" << weight << "kG" << endl;
+    }
+};
+
+int main()
+{
+    Dog dog;
+    dog.name = "旺财";
+    dog.getWeight(10);
+    dog.getWeight(10.5);
+    return 0;
+}
+```
+
+逐行解释:
+
+- 第 9 行:`getWeight(int weight)`,以 `int` 类型为参数。
+- 第 13 行:同名函数 `getWeight(double weight)`,参数类型不同,构成函数重载。
+- 第 22、23 行:分别传入 `int` 和 `double`,程序匹配不同的重载函数。
+
+
+注意:如果用户传入的参数类型不在任何重载函数里(例如 `string`),编译器匹配不到相应重载函数,**编译时就会报错**。可以多写几种类型(`string`、`char`、`float` 等)覆盖更多情况。
+
+#### 2.5.2 运算符重载(2.2.3.2)
+
+运算符重载的实质就是**函数重载或多态**,目的是让人能用同名的函数完成不同的基本操作。需要使用特殊函数形式:
+
+```cpp
+<返回类型说明符> operator <运算符符号>(<参数表>)
+{
+    <函数体>
+}
+```
+
+可重载的运算符:
+
+| 类别 | 运算符 |
+| ---- | ------ |
+| 双目算术 | `+` `-` `*` `/` `%` |
+| 关系 | `==` `!=` `<` `>` `<=` `>=` |
+| 逻辑 | `||` `&&` `!` |
+| 单目 | `+` `-` `*`(指针) `&`(取地址) |
+| 自增自减 | `++` `--` |
+| 位 | `\|` `&` `~` `^` `<<` `>>` |
+| 赋值 | `=` `+=` `-=` `*=` `/=` `%=` `&=` `\|=` `^=` `<<=` `>>=` |
+| 空间申请与释放 | `new` `delete` `new[]` `delete[]` |
+| 其他 | `()`(函数调用) `->`(成员访问) `,` `[]` |
+
+**不可重载**的运算符:
+
+| 运算符 | 含义 |
+| ------ | ---- |
+| `.` | 成员访问运算符 |
+| `.*` `->*` | 成员指针访问运算符 |
+| `::` | 域运算符 |
+| `sizeof` | 长度运算符 |
+| `?:` | 条件运算符 |
+| `#` | 预处理符号 |
+
+以重载 `+` 为例(`08_operator_example.cpp`),把两个 `Dog` 对象的体重相加,返回新的 `Dog`:
+
+```cpp
+#include <iostream>
+#include <string>
+using namespace std;
+
+class Dog
+{
+public:
+    int weight;
+    Dog operator+(const Dog &d) {
+        Dog dog;
+        dog.weight = this->weight + d.weight;
+        return dog;
+    }
+
+};
+
+int main()
+{
+    Dog dog1;
+    Dog dog2;
+    Dog dog3;
+
+    dog1.weight = 10;
+    dog2.weight = 20;
+    dog3 = dog1 + dog2;
+    cout << "第三只狗的体重是:" << dog3.weight << endl;
+    return 0;
+}
+```
+
+逐行解释:
+
+- 第 9~13 行:重载 `+` 运算符。成员函数形式下,左操作数是 `this`,右操作数是参数 `d`,使用 `this->weight` 访问左对象成员,返回一个新的 `dog` 对象。
+- 第 25 行:`dog1 + dog2` 触发重载后的 `+`,普通算术运算符 `+` 本来不能把两个对象相加,这正是重载的意义。
+
+
+实际开发中运算符重载用得较少,**理解这种思想即可**;但 Qt 里 `<<`(`cout` 插入)本身就是重载的典型例子。
+
+### 2.6 多态(2.2.4)
+
+C++ 多态意味着:调用成员函数时,会**根据调用函数的对象的类型**执行不同的函数。
+
+形成多态必须具备三个条件:
+
+1. 必须存在**继承关系**;
+2. 继承关系中必须有**同名虚函数**(虚函数是在基类中用关键字 `virtual` 声明的函数;在派生类中重新定义基类的虚函数时,会告诉编译器不要静态链接到该函数);
+3. 存在**基类类型的指针或引用**,通过该指针或引用调用虚函数。
+
+两个关键概念:
+
+- **虚函数**:基类中用 `virtual` 声明的函数。声明形式 `virtual ReturnType FunctionName(Parameter)`,**虚函数必须实现**,不实现编译器会报错。它让编译器不要静态链接,而改为**动态链接(后期绑定)**。
+- **纯虚函数**:基类中不能给出有意义实现时使用,声明形式 `virtual void function1() = 0;`。纯虚函数**一定没有定义**,用来规范派生类的行为(即接口)。包含纯虚函数的类是**抽象类**,抽象类不能实例化,但可以声明指向实现该抽象类的具体类的指针或引用。
+
+完整源码(`09_polymorphism_example.cpp`):
+
+```cpp
+#include <iostream>
+#include <string>
+using namespace std;
+
+/* 定义一个动物类 */
+class Animal
+{
+public:
+    virtual void run() {
+        cout << "Animal 的run()方法" << endl;
+    }
+};
+
+/* 定义一个狗类,并继承动物类 */
+class Dog : public Animal
+{
+public:
+    void run() {
+        cout << "Dog 的run()方法" << endl;
+    }
+
+};
+
+/* 定义一个猫类,并继承动物类 */
+class Cat : public Animal
+{
+public:
+    void run() {
+        cout << "Cat 的run()方法" << endl;
+    }
+
+};
+
+int main()
+{
+    /* 声明一个Animal 的指针对象,注:并没有实例化 */
+    Animal *animal;
+    /* 实例化dog 对象 */
+    Dog dog;
+    /* 实例化cat 对象 */
+    Cat cat;
+
+    /* 存储dog 对象的地址 */
+    animal = &dog;
+    /* 调用run()方法 */
+    animal->run();
+
+    /* 存储cat 对象的地址 */
+    animal = &cat;
+    /* 调用run()方法 */
+    animal->run();
+    return 0;
+}
+```
+
+逐行解释:
+
+- 第 9、18、28 行:三个类各有一个 `run()`。基类 `Animal` 的 `run()` 前面加了 `virtual`,成为虚函数。
+- 基类 `Animal` 声明指针 `animal`,通过基类指针访问 `Dog`、`Cat` 对象的 `run()`。**前提是基类的 `run()` 必须声明为虚函数**;若不声明,基类指针将访问到基类自己的 `run()`。
+- 第 44、49 行:把派生类对象地址赋给基类指针。当基类 `run()` 是虚函数时,编译器不静态链接到该函数,而是链接到派生类的 `run()`,实现运行时多态。
+
+
+可以尝试去掉 `virtual` 再编译:输出会变成两次 `Animal 的run()方法`,多态失效。
+
+虚函数表的机制(扩展):
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+编译器为含虚函数的类生成一张**虚函数表(vtable)**,对象里保存一个指向该表的指针(vptr)。运行时通过 vptr 找到实际类型对应的函数地址,从而实现动态绑定:
+
+```mermaid
+flowchart LR
+    animal["Animal* animal"] -->|指向| dogObj["Dog 对象"]
+    dogObj -->|vptr| vtable["Dog 的 vtable\nrun() -> Dog::run"]
+    animal2["Animal* animal"] -->|指向| catObj["Cat 对象"]
+    catObj -->|vptr| vtable2["Cat 的 vtable\nrun() -> Cat::run"]
+```
+
+这也解释了为什么**基类析构函数通常要声明为 virtual**:通过基类指针 `delete` 派生类对象时,只有虚析构才能正确调用到派生类析构,避免资源泄漏。
+
+### 2.7 数据封装(2.2.5)
+
+封装是面向对象编程中**把数据和操作数据的函数绑定在一起**的概念,能避免外界干扰和误用,从而确保安全。数据封装引申出"数据隐藏"。
+
+C++ 通过创建类来支持封装和数据隐藏,手段就是 `public`、`protected`、`private`。**通常情况下把类成员状态设为 `private`**,除非真的需要暴露,这样才能保证良好的封装性。
+
+完整源码(`10_encapsulation_example.cpp`):`addFood()` 作为对外接口(public),`total` 作为私有成员(private),通过 `getFood()` 读取:
+
+```cpp
+#include <iostream>
+#include <string>
+using namespace std;
+
+class Dog
+{
+public:
+    string name;
+
+    Dog(int i = 0)
+    {
+        total = i;
+    }
+
+    void addFood(int number) {
+        total = total + number;
+    }
+
+    int getFood() {
+        return total;
+    }
+private:
+    int total;
+};
+
+
+int main()
+{
+    Dog dog;
+
+    dog.name = "旺财";
+
+    dog.addFood(3);
+    dog.addFood(2);
+
+    cout << dog.name << "总共获得了" << dog.getFood() << "份食物" << endl;
+
+    return 0;
+}
+```
+
+逐行解释:
+
+- 第 10~13 行:在构造函数里初始化 `total`。不初始化的话 `total` 是随机的 `int` 值,所以一般都在构造函数里初始化;**不要在类内直接赋值初始化**,有可能某些编译器不支持。
+- 第 15~17 行:`addFood(int number)` 把获得的食物份数累加到 `total`。
+- 第 19~21 行:`getFood()` 返回食物总份数,通过它访问私有成员 `total`。
+- 第 8 行:`string name` 是 public 的成员,可以直接 `dog.name = "旺财"`。
+- 第 33、34 行:添加食物份数。
+- 第 36 行:打印总份数。
+
+
+### 2.8 数据抽象(2.2.6)
+
+数据抽象是指**只向外界提供关键信息,隐藏后台实现细节**,即只表现必要的信息而不呈现细节。它是一种依赖**接口和实现分离**的编程(设计)技术,好处:
+
+1. 类的内部受到保护,不会因无意的用户级错误导致对象状态受损;
+2. 类实现可以随时间变化,以应对新需求或缺陷修复,而不改变用户级代码。
+
+例子:手机能拍照、听音乐、收音,用户直接使用即可,不需要知道拍照是如何通过摄像头取像、如何在屏幕上显示的。C++ 类为数据抽象提供了可能:向外界提供大量用于操作对象数据的**公共方法**,外界并不清楚类的内部实现。
+
+`cout` 本身就是一个公共接口,我们不必知道它如何在屏幕上显示内容,底层已经实现好了。
+
+数据封装与数据抽象的区别:封装是把数据和操作数据的函数**捆绑**在一起的机制;抽象是仅向用户**暴露接口**而隐藏实现细节的机制。带公有和私有成员的类都可以作为数据抽象的实例(例子参考 2.7)。
+
+### 2.9 接口(抽象类)(2.2.7)
+
+接口**描述类的行为和功能,而不需要完成类的特定实现**。C++ 接口是用**抽象类**实现的:
+
+- 类中至少有一个函数被声明为**纯虚函数**,这个类就是抽象类。纯虚函数通过声明中的 `= 0` 指定。
+- 设计抽象类(通常称 ABC)的目的,是为其他类提供一个可继承的适当基类。
+- 抽象类**不能实例化对象**,只能作为接口使用;试图实例化抽象类对象会导致编译错误。
+- 子类若要被实例化,**必须实现每个虚函数**;没有重写纯虚函数就尝试实例化该类的对象,也会导致编译错误。
+- 可用于实例化对象的类称为**具体类**。
+
+完整源码(`11_abstract_class.cpp`):与多态例程类似,只是 `Animal` 的 `run()` 定义为纯虚函数,不用实现,由 `Dog`、`Cat` 重写:
+
+```cpp
+#include <iostream>
+
+using namespace std;
+
+/* 定义一个动物类 */
+class Animal
+{
+public:
+    virtual void run() = 0;
+};
+
+/* 定义一个狗类,并继承动物类 */
+class Dog : public Animal
+{
+public:
+    void run() {
+        cout << "Dog 的run()方法" << endl;
+    }
+
+};
+
+/* 定义一个猫类,并继承动物类 */
+class Cat : public Animal
+{
+public:
+    void run() {
+        cout << "Cat 的run()方法" << endl;
+    }
+
+};
+
+int main()
+{
+    /* 实例化dog 对象 */
+    Dog dog;
+
+    /* 实例化cat 对象 */
+    Cat cat;
+
+    /*  dog 调用run()方法 */
+    dog.run();
+
+    /* cat 调用run()方法 */
+    cat.run();
+
+    return 0;
+}
+```
+
+
+结果与 2.6 的多态例程相同,但体现了**两种不同的思想**:多态例程是"基类给默认实现,派生类可选择重写",抽象类例程是"基类只给接口,派生类必须实现"。
+
+多态与抽象类对比:
+
+| 对比项 | 多态(虚函数有实现) | 接口(纯虚函数) |
+| ------ | -------------------- | ---------------- |
+| 基类函数 | `virtual void run() {...}` | `virtual void run() = 0;` |
+| 基类可实例化 | 可以 | **不可以**(抽象类) |
+| 派生类是否必须重写 | 否,不重写就用基类实现 | 是,否则派生类也无法实例化 |
+| 典型用途 | 提供可覆盖的默认行为 | 定义统一接口/规范 |
+
+Qt 中大量使用这两者:继承 `QMainWindow` 时可以重写 `paintEvent()` 等虚函数;而 `QAbstractItemModel` 这类抽象基类必须实现其纯虚接口才能使用。
+
+---
+
+## 3. 编译与运行
+
+### 3.1 通用流程
+
+所有例程都是**单文件 C++ 程序**(无 `.h`、无 `.pro`),在 Ubuntu 终端即可完成:
+
+```bash
+# 1. 创建目录并进入
+mkdir -p C++/03_class_dog_example && cd C++/03_class_dog_example
+
+# 2. 编辑源文件(vi/vim 或任意编辑器)
+vi 03_class_dog_example.cpp
+
+# 3. 编译
+g++ 03_class_dog_example.cpp -o 03_class_dog_example
+
+# 4. 运行
+./03_class_dog_example
+```
+
+### 3.2 例程与源码文件对照
+
+11 个例程(`Embedded-Qt-Tutorial/C++/`)与本文 2.x 各小节一一对应:`01_hello_world`(Hello World)、`02_namespace_example`(命名空间)、`03_class_dog_example`(类与对象)、`04_structor_example`(构造/析构)、`05_this_pointer_example`(this 指针)、`06_inherit_example`(继承)、`07_func_overloading`(函数重载)、`08_operator_example`(运算符重载)、`09_polymorphism_example`(多态)、`10_encapsulation_example`(数据封装)、`11_abstract_class`(抽象类/接口)。
+
+### 3.3 常见编译问题
+
+| 现象 | 可能原因 | 排查手段 |
+| ---- | -------- | -------- |
+| `undefined reference to ...` | 函数只声明未定义 | 检查是否写了函数体;构造函数/析构函数是否在类外漏写 `Dog::` |
+| `error: 'cout' was not declared` | 缺少头文件或命名空间 | 加 `#include <iostream>` 与 `using namespace std;` |
+| `error: 'string' was not declared` | 漏包含 `<string>` | 加 `#include <string>` |
+| `cannot be used as a function` | 变量名与函数名冲突或少了分号 | 检查类定义末尾的 `};` |
+| 多态未生效 | 基类函数漏写 `virtual` | 给基类函数加 `virtual` |
+| 内存/段错误 | 堆对象未 `delete` 或野指针 | `new` 配 `delete`,释放后置 `NULL` |
+
+---
+
+## 4. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+### Q1:构造函数和析构函数各有什么特点?什么时候被调用?
+
+**答**:构造函数与类名同名、可重载、无返回类型;在对象实例化时由系统自动调用,且仅调用一次,通常用来初始化成员。析构函数格式为 `~类名()`、不能带参数、无返回值;在对象生命周期结束时自动调用,用于释放内存/资源。若用户未定义,编译器会自动生成一个空实现。栈对象离开作用域时析构,堆对象 `delete` 时析构。
+
+### Q2:`this` 指针是什么?能在静态函数里用吗?
+
+**答**:`this` 指向当前类的当前实例对象,记录对象内存地址,每个对象各有一个。成员函数通过它区分"操作的是哪个对象的数据"。`this` 只能在非静态成员函数中使用,全局函数和静态函数中都不能使用(静态函数不依赖具体对象)。它在成员函数开始时构造、结束时清除,存放位置由编译器决定(栈、寄存器或全局变量)。
+
+### Q3:函数重载和运算符重载分别是什么?重载决策如何进行?
+
+**答**:两者都是同一作用域内为同名函数/运算符提供多个定义。函数重载要求形参的个数、类型或顺序不同,**不能仅靠返回类型不同来区分**。运算符重载使用 `operator` 加运算符符号的特殊函数形式,把运算符作用于自定义类型时转成函数调用;`.`、`::`、`sizeof`、`?:`、`#` 等不可重载。调用时编译器用实参类型与各定义形参比较,选出最合适的定义,这一过程叫重载决策。
+
+### Q4:什么是多态?形成多态需要哪些条件?虚函数和纯虚函数有何区别?
+
+**答**:多态指调用成员函数时根据**对象的实际类型**执行不同的函数。三个条件:存在继承关系;派生类与基类有同名虚函数(基类用 `virtual` 修饰);通过基类指针或引用调用虚函数。虚函数在基类中必须实现,派生类可选择重写;纯虚函数用 `= 0` 声明、没有实现,含有纯虚函数的类是抽象类,不能实例化,派生类必须实现所有纯虚函数才能实例化。本质上虚函数通过虚函数表实现动态(后期)绑定。
+
+### Q5:封装、抽象、抽象类三者的联系与区别是什么?
+
+**答**:封装是把数据和操作数据的函数捆绑在一起,并用 `public`/`protected`/`private` 控制访问,通常数据成员设为 `private`、通过公共接口操作,从而保护内部状态;抽象是只暴露接口、隐藏实现细节,是一种接口与实现分离的设计技术。二者的实例都是"带公有和私有成员的类"。抽象类是实现接口的 C++ 手段:只要类中有至少一个纯虚函数它就是抽象类,不能实例化,只能作为基类/接口被继承,派生类实现其全部纯虚函数后才能实例化。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Qt开发指南V1.2》第一章 在Ubuntu下编写C++、第二章 C++基础,以及正点原子 Qt 开发指南例程源码 `Embedded-Qt-Tutorial/C++/01_hello_world` ~ `11_abstract_class`。

+ 794 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/02-Qt入门与项目结构.md

@@ -0,0 +1,794 @@
+---
+title: Qt入门与项目结构
+tags: [嵌入式Linux, Qt, QtCreator, qmake, pro文件, ui文件, QtDesigner, moc, qrc, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第三章 初识Qt、第四章 使用Qt Designer开发;例程 Embedded-Qt-Tutorial/Qt/01/01_hello_world、02_designer_example"
+---
+
+# Qt入门与项目结构
+
+> 💡 **关联知识**:[[05-Qt应用开发/01-C++基础与面向对象]](Qt 项目里的继承、虚函数、`this` 全部来自 C++)、[[05-Qt应用开发/03-信号与槽]](Qt Designer 里连接的信号槽,正是下一篇的主题);延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/1. Qt概述]]、[[编程学习项目/使用Qt和C++构造UI界面/2. 创建Qt项目]]
+
+本篇解决"从零跑起一个 Qt 程序"的全部问题:Qt 到底是什么、和 Qt Creator 什么关系、怎么装、怎么新建项目、项目里每个文件是干嘛的、`qmake && make` 背后发生了什么。最后用 Qt Designer 做一个"点击按钮关闭窗口"的完整例子,并首次触碰信号与槽。
+
+配置 Qt Creator 的中文输入法是新手最容易卡住的一步,本篇按原书流程完整保留。
+
+---
+
+## 1. Qt 是什么
+
+### 1.1 Qt 的定位
+
+Qt 是一个**跨平台的 C++ 开发库**,主要用来开发图形用户界面(Graphical User Interface,GUI)程序。但 GUI 并不是 Qt 的全部,它还内置了大量非界面能力:
+
+| 能力类别 | 说明 |
+| -------- | ---- |
+| 图形界面 | 控件、布局、交互、绘图 |
+| 多线程 | `QThread` 等 |
+| 数据库 | Qt SQL |
+| 图像处理 | `QImage`、`QPixmap`、`QPainter` |
+| 音频视频 | Qt Multimedia |
+| 网络通信 | `QTcpSocket`、`QUdpSocket` |
+| 文件操作 | `QFile`、`QTextStream` |
+
+Qt 还存在 Python、Ruby、Perl 等脚本语言的绑定;支持的操作系统非常多,例如通用操作系统 Windows、Linux、Unix,智能手机系统 Android、iOS、WinPhone,嵌入式系统 QNX、VxWorks 等。
+
+Qt 最让人喜欢的特性是**跨平台**:一份代码无需修改或只需小修改,就能在其他平台运行。
+
+### 1.2 Qt 与 Qt Creator 的关系
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+原书 3.1.1 小节仅列出标题未展开正文。这两个名字初学者最容易混淆:
+
+| 名称 | 是什么 | 类比 |
+| ---- | ------ | ---- |
+| **Qt** | 跨平台 C++ **开发库**(一堆 `.so`/`.dll` + 头文件 + 工具链) | 相当于"语言的标准库" |
+| **Qt Creator** | 官方出品的 **IDE(集成开发环境)**,用来写代码、编译、调试、设计界面 | 相当于"编辑器 + 构建器 + 调试器" |
+
+关系可以这样理解:**Qt Creator 自带的 Qt Designer(UI 设计器)也只是它的一个功能模块**,属于 Qt Creator,不要和 Qt 库混为一谈。没有 Qt Creator,用 `vim` + `qmake` + `make` 一样能开发 Qt 程序,只是没有可视化界面而已。
+
+```mermaid
+flowchart TB
+    subgraph QtLib["Qt(开发库)"]
+        Core["QtCore / QtGui / QtWidgets ..."]
+        Tools["qmake / moc / uic / rcc"]
+    end
+    subgraph QTC["Qt Creator(IDE)"]
+        Editor["代码编辑器"]
+        Designer["Qt Designer(UI 设计器)"]
+        Debug["调试器"]
+        Build["构建与运行"]
+    end
+    QTC -->|调用| QtLib
+```
+
+### 1.3 Qt 能做什么
+
+理论上你能想到的 App,Qt 基本都能做:WPS 这类大型桌面应用、Maya(3D 建模)、一些游戏内核都能用 Qt 实现,也可以快速开发音乐播放器。在嵌入式里,用 Qt 开发界面已几乎是主流,工控界面最常用,移动端界面(点菜机、温度采集显示、汽车仪表、速度显示)也开始用 Qt。
+
+原书举例的界面有:暗黑主题音乐播放器、"炫酷车载音乐 APP 主界面"等。作者的提醒很实在:**设计界面占用的时间可能是 70%,写程序占 30%**,做好界面要有想法,最好有一点美工基础。
+
+### 1.4 Qt/C++ 与 QML
+
+- **QML** 是在 Qt C++ 基础上拓展的声明式语言,Qt C++ 上使用的界面类基本都能拓展到 QML;不能拓展的也可以 QML 与 C++ 混用。
+- Qt QML 模块为 QML 开发提供框架;**QML 本身不涉及显示部分**,由 Qt Quick 模块承担显示。
+- **Qt C++ 效率更高**,在底层编写功能更突出;要做好看又功能强的界面,Qt C++ 与 QML 结合是必不可少的——QML 负责界面层,C++ 负责底层(文件读写、图像处理、多线程通信等)。
+- 低版本 Qt(如 Qt5.6 以下)QML 不够成熟,高版本(Qt5.7 以上)更成熟。C++ 依旧是 Qt 的主要编程语言,Qt5 也添加了很多新的 C++ API。
+
+本教程以 **Qt C++ 为主**,QML 作为加分项。
+
+---
+
+## 2. 如何选择 Qt 版本(3.2)
+
+选择版本的总原则:**不要盲目追最新,选长期支持(LTS)版本**。
+
+| 版本 | 说明 |
+| ---- | ---- |
+| Qt4 / Qt5 | 信号槽写法有区别(Qt4 的旧写法本教程不讲解) |
+| Qt5.6 / Qt5.9 | Qt 长期支持版本(LTS,Long Term Support) |
+| Qt5.12 | LTS 版本,**本教程以 Qt5.12.9 开发及实验** |
+| Qt6 | 理论上与 Qt5 兼容,但仍需时间普及;许多公司仍在用 Qt5 甚至 Qt4 |
+
+几点说明:
+
+- Qt5.15 是截至原书成书时较新的 Qt5 版本。
+- Qt 官方曾把 Qt5.2~Qt5.8、Qt5.11 归档,老版本要到归档地址下载。
+- 程序写好后要长期运行在某个确定版本里,避免随意升级带来兼容性问题或重复移植。
+- 原书成书时 Qt 官网部分地址国内访问受限,教程使用离线安装包 `Qt5.12.9`(位于资料网盘)。
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+在线安装早已改为 Qt 官方 Online Installer,离线包路径也可能变化;下载链接会变动,选版本原则(LTS、固定版本)仍然适用。
+
+---
+
+## 3. 安装 Qt
+
+### 3.1 Windows 下安装概要(3.3)
+
+流程概括为:登录 Qt 帐号 → 同意开源许可(开源版遵循 GPLv2 / GPLv3 / LGPLv3)→ 修改安装目录 → **选择组件** → 安装 → 完成时勾选启动 Qt Creator。
+
+Windows 组件选择要点:
+
+| 组件 | 说明 |
+| ---- | ---- |
+| MSVC | 微软 VC 编译器,需要额外安装 VC 相关库 |
+| **MinGW** | Minimalist GNU on Windows,可在 GNU/Linux 与 Windows 上生成 Windows 程序,**教程只勾选 MinGW** |
+| Android / ARM 等 | 做嵌入式交叉编译时才需要 |
+| Sources | Qt 源码,可按需选择 |
+| Source 以下 | Qt 的各个模块,可按需勾选 |
+
+安装完成后打开 Qt Creator 即可。
+
+### 3.2 Linux 下安装(3.4)
+
+在 Ubuntu 上下载 `.run` 安装包,赋予可执行权限后以 `sudo` 安装,默认装到 `/opt` 目录:
+
+```bash
+# 下载(原书给的链接,实际可能已失效,使用离线包即可)
+wget http://download.qt.io/archive/qt/5.12/5.12.9/qt-opensource-linux-x64-5.12.9.run
+
+# 赋予可执行权限
+chmod +x qt-opensource-linux-x64-5.12.9.run
+
+# 以 sudo 安装,这样会安装到 /opt 目录下
+sudo ./qt-opensource-linux-x64-5.12.9.run
+```
+
+安装界面与 Windows 一致,选择默认安装目录即可。安装组件时,**务必勾选 `Desktop Qt5.12.9 GCC 64bit`**,否则 Qt Creator 里检测不到编译工具(Kit)。
+
+安装完成后,在左下角应用程序中心找到 Qt Creator 打开。
+
+> **注意**:`/opt` 目录普通用户只有读权限,没有写权限。所以 Qt Creator 里打开自带示例时要选"复制项目并打开"。
+
+### 3.3 配置 Qt Creator 中文输入(3.4.2)
+
+Ubuntu 默认是英文环境,且需要中文输入法才能在 Qt Creator 里写中文注释。
+
+**第一步:配置 Ubuntu 中文环境**
+
+1. 右上角点击设置图标 → 区域与语言 → 管理已安装的语言(install/Remove Languages…)。
+2. 在语言列表中选择"简体中文",点击 Apply。
+3. 重启或注销 Ubuntu。系统询问是否把系统文件夹名称也改成中文时,**建议选择"否"**,保留英文名,便于终端操作。
+
+**第二步:配置中文输入法**
+
+1. 在"区域与语言"的键盘输入法系统(Keyboard input method system)中,确认设置为 **IBus**(Ubuntu 默认即为 IBus,没改过就不用动)。
+2. 回到区域与语言处,检查"输入源"中是否有"中文(智能拼音)"。若没有,点击"+"添加输入源 → 汉语 → 中文(智能拼音),也可以按习惯选择五笔等 IBus 输入法。
+3. 让配置生效:**建议重启 Ubuntu**,防止环境未生效。
+
+验证:在终端里切换到中文输入,能打出汉字即成功。中英文切换一般按 `Win + 空格`(不同 Ubuntu 版本可能不同)。之后在 Qt Creator 中即可用 IBus 拼音输入法输入中文。
+
+---
+
+## 4. Qt Creator 简单使用
+
+### 4.1 界面组成(3.5.1)
+
+启动 Qt Creator 后默认打开**欢迎页面**,左侧栏主要按钮:
+
+| 区域 | 作用 |
+| ---- | ---- |
+| 欢迎 | 打开示例、最近项目、入门教程 |
+| 编辑 | 写代码、编辑 `.ui`(切到设计器) |
+| 设计 | UI 设计界面 |
+| 调试 | 调试 |
+| 项目 | 构建设置 |
+| **帮助** | Qt 官方文档,可搜索类、方法、信号与槽,**是学习 Qt 最重要的资料** |
+
+Qt Creator 自带很多示例。由于在 Ubuntu 里 Qt 安装在 `/opt` 下没有写权限,打开示例时要先选"**复制项目并打开**"。
+
+菜单"帮助 → UI Tour"会介绍 Qt Creator 各组件,建议看一看(英文)。
+
+### 4.2 设置(3.5.2)
+
+顶部菜单"工具 → 选项"。常用设置项:
+
+| 设置项 | 作用 |
+| ------ | ---- |
+| **Kits** | 显示编译工具(如 `Desktop Qt5.12.9 GCC 64bit`);也可在此配置 ARM 平台编译器,Qt 跨平台正靠不同平台的编译工具实现 |
+| 环境 | 设置主题、语言等(默认即可) |
+| 文本编辑器 | 设置字体大小、颜色,以及关键字/字符串/注释的配色 |
+| 构建和运行 | 常用的是设置项目的构建目录,其余默认即可 |
+
+---
+
+## 5. 第一个 Qt 程序(3.6)
+
+### 5.1 新建项目
+
+1. 打开 Qt Creator,菜单"文件 → 新建文件或项目",快捷键 `Ctrl + N`(需在英文输入法下)。
+2. 模板选择 **Application (Qt) → Qt Widgets Application**。模板会把 `.cpp`、`.ui` 等文件都快速创建好,无需手动一个个加。
+3. 项目名称输入 `01_hello_world`,选择项目路径(可勾选"设为默认的项目路径"),下一步。
+4. 构建系统选择 **qmake**(用 qmake 生成 Makefile 用于编译),下一步。
+5. 基类(Base class)选择 **QMainWindow**,勾选"Generate form"(生成 `mainwindow.ui`),下一步。
+6. 勾选编译器(安装组件时选的那个,编译出 Ubuntu 上运行的程序),下一步。
+7. 版本控制选择"无",点击完成。
+
+**基类怎么选**:
+
+| 基类 | 说明 | 适用场景 |
+| ---- | ---- | -------- |
+| `QMainWindow` | 主窗口类,带主菜单栏、工具栏、状态栏 | 桌面式主窗口程序 |
+| `QWidget` | 可视界面类的基类;`QMainWindow` 也是由 `QWidget` 继承封装而来,功能更少 | **嵌入式常用**(嵌入式一般不需要标题栏、状态栏) |
+| `QDialog` | 对话框类 | 较少作为主基类 |
+
+### 5.2 项目文件介绍(3.6.2)
+
+新建项目后,左侧项目文件结构分组如下:
+
+| 文件/分组 | 内容 |
+| --------- | ---- |
+| `01_hello_world.pro` | **项目管理文件**,加入/删除文件时 Qt Creator 会自动修改它;有时需手动打开添加设置项 |
+| Header 分组 | 项目内所有头文件 `*.h` |
+| Source 分组 | 项目内所有 C++ 源码 `*.cpp` |
+| Forms 分组 | 项目内所有界面文件 `*.ui`(XML 描述,编译时生成相应 cpp 文件供交叉编译器编译) |
+
+项目名以粗体显示表示它是**活动项目**。
+
+#### 5.2.1 项目文件 `*.pro`(3.6.2.1)
+
+```pro
+QT       += core gui
+
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+
+CONFIG += c++11
+
+# The following define makes your compiler emit warnings if you use
+# any Qt feature that has been marked deprecated (the exact warnings
+# depend on your compiler). Please consult the documentation of the
+# deprecated API in order to know how to port your code away from it.
+DEFINES += QT_DEPRECATED_WARNINGS
+
+# You can also make your code fail to compile if it uses deprecated APIs.
+# In order to do so, uncomment the following line.
+# You can also select to disable deprecated APIs only up to a certain version of Qt.
+#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0
+
+SOURCES += \
+    main.cpp \
+    mainwindow.cpp
+
+HEADERS += \
+    mainwindow.h
+
+FORMS += \
+    mainwindow.ui
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target
+```
+
+逐行解释:
+
+- 第 1 行:添加 Qt 支持的模块,`core` 与 `gui` 是 Qt 默认设置。
+- 第 3 行:比较 Qt5 版本,如果是 Qt5,`application` 在 `QtWidgets` 中,因此要加上 `widgets` 库。
+- 第 5 行:配置使用 C++11。
+- 第 11 行:添加 `QT_DEPRECATED_WARNINGS` 定义(使用被标记为过时的 API 时给出警告)。
+- 第 18 行:`SOURCES` 下的源文件列表。
+- 第 22 行:`HEADERS` 下的头文件列表。
+- 第 25 行:`FORMS` 下的 ui 界面文件列表;若项目不需要 ui 文件,这里留空即可。
+- 第 28 行起:部署默认规则。`qnx` 判断是不是 qnx 操作系统,赋值 `target.path`;如果是 unix 但不是 android,赋值 `/opt/$${TARGET}/bin`;若 `target.path` 非空则 `INSTALLS += target`。
+
+若要修改生成的可执行文件名,可加 `TARGET = xxx`;否则 `TARGET` 默认取项目名。
+
+#### 5.2.2 样式文件 `*.ui`(3.6.2.2)
+
+`mainwindow.ui` 是 XML 文件,**由 Qt Designer 生成,不要手动编辑**,只能通过图形界面修改属性:
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>MainWindow</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <widget class="QLabel" name="label">
+    <property name="geometry">
+     <rect>
+      <x>260</x>
+      <y>160</y>
+      <width>101</width>
+      <height>17</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>Hello World!</string>
+    </property>
+   </widget>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>800</width>
+     <height>28</height>
+    </rect>
+   </property>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <resources/>
+ <connections/>
+</ui>
+```
+
+双击 `.ui` 文件进入设计界面,主要区域:
+
+| 区域 | 说明 |
+| ---- | ---- |
+| ① 控件栏 | 各种控件;上方 Filter 输入首字母可快速定位 |
+| ② 设计窗体 | 显示窗口程序,默认 MainWindow 带菜单栏和状态栏 |
+| ③ 对象栏 | ②中用到的对象都在此显示(树状结构) |
+| ④ 属性栏 | 选中③中对象后,在此编辑其属性(位置、大小、文字、颜色、字体等) |
+
+#### 5.2.3 头文件 `*.h`(3.6.2.3)
+
+`mainwindow.h` 一般有一个对应的 `mainwindow.cpp`:`.h` 放**类的声明**,`.cpp` 放**类的实现**,这与前面 C++ 的知识一致。
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }
+QT_END_NAMESPACE
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    Ui::MainWindow *ui;
+};
+#endif // MAINWINDOW_H
+```
+
+逐行解释:
+
+- 第 7 行:定义命名空间 `Ui`,里面有一个类 `MainWindow`,它和第 10 行的 `MainWindow` **不是同一个**。本文件里的 `MainWindow` 类有一个成员 `*ui`,指向 `Ui::MainWindow` 的指针,这都是为了使用 `.ui` 文件设计界面。
+- 第 12 行:`Q_OBJECT` 是一个宏,由 Qt 处理(moc),这是 Qt 对 C++ 的扩展;**所有用到信号与槽的类都要加这个宏**。
+
+#### 5.2.4 源文件 `*.cpp`(3.6.2.4)
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+    , ui(new Ui::MainWindow)
+{
+    ui->setupUi(this);
+}
+
+MainWindow::~MainWindow()
+{
+    delete ui;
+}
+```
+
+逐行解释:
+
+- 第 2 行:`ui_mainwindow.h` 是 **Qt 根据 `.ui` 文件自动生成的**,构建/编译后才在 `debug`/`release` 目录出现。
+- 第 4~6 行:构造函数用初始化列表,`ui(new Ui::MainWindow)` 是初始化成员的方法。
+- 第 8 行:`ui->setupUi(this);` 进行界面初始化,把 `this`(MainWindow 本身)作为参数传入,ui 里的对象以 `this` 为父对象,所有子对象都显示在 MainWindow 里。**要使用 ui 里的对象,代码必须写在 `ui->setupUi(this)` 之后**。
+- 第 13 行:析构函数里 `delete ui`。**在 Qt 里需要在析构函数 delete 的对象,一般是 new 出来且没有父对象的对象**(有父对象的会由父对象自动释放)。
+
+`main.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    MainWindow w;
+    w.show();
+    return a.exec();
+}
+```
+
+逐行解释:
+
+- 第 3 行:包含 `QApplication` 类定义。**每个使用 Qt 的应用程序都必须有一个 QApplication 对象**,它管理各种应用程序的广泛资源(如默认字体和光标)。
+- 第 5 行:`main()` 是程序入口。`argc` 是命令行变量数量,`argv` 是命令行变量数组。
+- 第 7 行:创建 `QApplication` 对象 `a`,处理命令行变量。
+- 第 8 行:创建 `MainWindow` 对象 `w`。
+- 第 9 行:`w.show()`,程序界面才能显示。
+- 第 10 行:把控制转交给 Qt,应用程序退出时 `exec()` 返回。`exec()` 中 Qt 接受并处理用户和系统事件,并传递给适当的窗口部件。
+
+### 5.3 修改 ui 文件显示 Hello World(3.6.3)
+
+1. 双击 `mainwindow.ui` 进入 UI 设计器。
+2. 在左侧控件栏找到 **Label** 控件(在 "Display Widgets" 分组下)。
+3. 把 Label 拖到中间的设计窗体。
+4. 在对象栏选中 `label`,在属性栏找到 `text` 属性,修改为 `Hello World!`。也可直接双击 Label 修改文本。
+5. 保存。
+
+### 5.4 项目编译、调试、运行(3.6.4)
+
+**编译运行**:
+
+- 点击左下角绿色三角按钮,或按 `Ctrl + R`(编译且运行)。
+- 编译输出窗口可以看到编译警告、错误、链接和相关编译器过程。
+- 第一次编译常见错误 `Cannot find -lGL`:Qt5.0 的库默认链接到 OpenGL,而 Ubuntu 没装 OpenGL。解决:
+
+```bash
+sudo apt-get install libglu1-mesa-dev
+```
+
+若还有其他报错,回到 C++ 环境设置补装相应库——**Qt 也依赖 C++ 环境**。
+
+**调试**:关闭已运行的窗口,在代码里右键设置断点,点击左下角带 Debug 的绿色三角按钮即可调试。简单程序一般不需要,但可用来观察 Qt 程序运行流程。
+
+---
+
+## 6. 构建流程与项目结构总结
+
+### 6.1 qmake 构建链路
+
+`.pro` 文件本身不是 Makefile,qmake 先把它翻译成 Makefile,再由 make 驱动编译器。过程中还涉及三个工具:
+
+| 工具 | 输入 | 输出 | 作用 |
+| ---- | ---- | ---- | ---- |
+| **qmake** | `*.pro` | `Makefile` | 根据工程配置生成构建规则 |
+| **moc** | 含 `Q_OBJECT` 的 `*.h` | `moc_*.cpp` | 元对象编译器,实现信号槽等元对象能力 |
+| **uic** | `*.ui` | `ui_*.h` | 把界面 XML 转成 C++ 头文件 |
+| **rcc** | `*.qrc` | `qrc_*.cpp` | 把资源文件编译进程序 |
+
+```mermaid
+flowchart LR
+    PRO["*.pro"] -->|qmake| MK["Makefile"]
+    UI["*.ui"] -->|uic| UIH["ui_mainwindow.h"]
+    H["mainwindow.h(Q_OBJECT)"] -->|moc| MOC["moc_mainwindow.cpp"]
+    QRC["*.qrc"] -->|rcc| RCC["qrc_resources.cpp"]
+    MK -->|make| BIN["可执行程序"]
+    UIH --> BIN
+    MOC --> BIN
+    RCC --> BIN
+    CPP["*.cpp"] --> BIN
+```
+
+### 6.2 命令行构建步骤
+
+不打开 Qt Creator 时,用 `qmake && make` 即可构建:
+
+```bash
+cd 01_hello_world
+/opt/Qt5.12.9/5.12.9/gcc_64/bin/qmake 01_hello_world.pro   # 生成 Makefile
+make                                                       # 编译链接
+./01_hello_world                                           # 运行
+```
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+Qt 安装路径随版本和安装方式变化,可用 `which qmake` 或 `qmake -v` 确认;交叉编译时把 `qmake` 换成目标平台的 qmake 即可(见 [[05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战]])。
+
+### 6.3 项目文件一览
+
+| 文件 | 由谁生成 | 是否手改 | 作用 |
+| ---- | -------- | -------- | ---- |
+| `*.pro` | Qt Creator | 可小改 | 工程配置:模块、源文件、编译选项 |
+| `*.ui` | Qt Designer | 不手改 | 界面 XML,uic 转成 `ui_*.h` |
+| `*.h` | 开发者 | 改 | 类声明、信号槽声明 |
+| `*.cpp` | 开发者 | 改 | 类实现 |
+| `ui_*.h` | uic | 不手改 | 构建时生成,位于 build 目录 |
+| `moc_*.cpp` | moc | 不手改 | 构建时生成 |
+| `*.qrc` | 开发者 | 可改 | 资源索引,rcc 编译进程序 |
+
+### 6.4 资源文件 `.qrc`
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+`.qrc` 会在后续控件章节(第七章)的例程中实际使用。
+
+Qt 资源系统(The Qt Resource System)把图片、图标、字体、翻译文件等**打包进可执行文件**,程序运行时不再依赖外部文件路径,非常适合嵌入式。资源索引文件以 `.qrc` 为后缀,也是 XML 格式,例如某控件例程的 `res.qrc`:
+
+```xml
+<RCC>
+    <qresource prefix="/">
+        <file>images/openedv.png</file>
+    </qresource>
+</RCC>
+```
+
+- `<qresource prefix="/">`:资源在程序内的访问前缀。
+- `<file>images/openedv.png</file>`:相对于 `.qrc` 文件的资源路径。
+- 程序中用 `":/images/openedv.png"` 访问。
+- 新加入资源后,需要在 `.pro` 里声明 `RESOURCES += res.qrc`(Qt Creator 添加资源时通常会自动写入),然后 `rcc` 会生成 `qrc_*.cpp` 参与编译。
+
+---
+
+## 7. 使用 Qt Designer 开发(第四章)
+
+Qt Designer 是 Qt Creator 的一个功能,也叫 UI 设计器。它以"连接信号与槽"为例展示可视化开发流程。
+
+### 7.1 在 UI 文件添加一个按钮(4.1.1)
+
+新建项目 `02_designer_example`(步骤同 3.6)。在左侧找到 **Push Button**,拖到中间窗体;在属性栏把它的 `text` 属性改为"关闭程序"。
+
+### 7.2 在 UI 文件里连接信号与槽(4.1.2)
+
+先建立直观认识:**信号**是对象在特定情况下发出的"事件";**槽**是信号发出时被触发执行的函数。在 Qt 里要想让事件做出对应动作,就必须用信号与槽(下一章详述)。
+
+**方法一:在 UI 设计器里可视化连接**
+
+1. 点击设计器顶部的"信号槽连接"按钮进入连接模式(退出点它左边的按钮)。
+2. 选中"关闭程序"按钮,按住鼠标向外拖动,出现信号槽连接符号。
+3. 左侧选择 `pushButton` 的 `clicked()` 信号,右侧选择 `MainWindow` 的 `close()` 槽,完成连接。
+4. 切回"编辑部件"模式。
+
+含义:单击"关闭程序"按钮时发出 `clicked()` 信号,触发 `MainWindow` 的 `close()` 槽,从而关闭程序。
+
+连接保存在 `.ui` 文件的 `<connections>` 节点里:
+
+```xml
+ <connections>
+  <connection>
+   <sender>pushButton</sender>
+   <signal>clicked()</signal>
+   <receiver>MainWindow</receiver>
+   <slot>close()</slot>
+  </connection>
+ </connections>
+```
+
+编译后这些连接会变成 `ui_mainwindow.h` 中 `setupUi()` 里的 `QObject::connect(...)` 语句。
+
+**方法二:右键"转到槽"**
+
+1. 选中"关闭程序"按钮,右键 → "转到槽"。
+2. 弹出窗口选择信号 `clicked()`。注意这个 `clicked()` 并不属于 `QPushButton` 而是属于 **`QAbstractButton`**,`QPushButton` 继承下来即可使用——这正是 C++ 继承在 Qt 里的体现,**根本不用重写 clicked() 事件**。
+3. 点击 OK 后,Qt Creator 自动在 `mainwindow.h` 声明槽函数,并在 `mainwindow.cpp` 生成函数框架。
+4. 在生成的 `on_pushButton_clicked()` 槽函数里写 `this->close();`。
+
+### 7.3 完整例程源码
+
+`02_designer_example.pro`(与 01 例程结构相同):
+
+```pro
+QT       += core gui
+
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+
+CONFIG += c++11
+
+DEFINES += QT_DEPRECATED_WARNINGS
+
+SOURCES += \
+    main.cpp \
+    mainwindow.cpp
+
+HEADERS += \
+    mainwindow.h
+
+FORMS += \
+    mainwindow.ui
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target
+```
+
+`mainwindow.ui`(含按钮与信号槽连接):
+
+```xml
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>MainWindow</class>
+ <widget class="QMainWindow" name="MainWindow">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>800</width>
+    <height>600</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>MainWindow</string>
+  </property>
+  <widget class="QWidget" name="centralwidget">
+   <widget class="QPushButton" name="pushButton">
+    <property name="geometry">
+     <rect>
+      <x>260</x>
+      <y>150</y>
+      <width>89</width>
+      <height>25</height>
+     </rect>
+    </property>
+    <property name="text">
+     <string>关闭程序</string>
+    </property>
+   </widget>
+  </widget>
+  <widget class="QMenuBar" name="menubar">
+   <property name="geometry">
+    <rect>
+     <x>0</x>
+     <y>0</y>
+     <width>800</width>
+     <height>28</height>
+    </rect>
+   </property>
+  </widget>
+  <widget class="QStatusBar" name="statusbar"/>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>pushButton</sender>
+   <signal>clicked()</signal>
+   <receiver>MainWindow</receiver>
+   <slot>close()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>325</x>
+     <y>184</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>379</x>
+     <y>106</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
+```
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }
+QT_END_NAMESPACE
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private slots:
+    void on_pushButton_clicked();
+
+private:
+    Ui::MainWindow *ui;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+    , ui(new Ui::MainWindow)
+{
+    ui->setupUi(this);
+}
+
+MainWindow::~MainWindow()
+{
+    delete ui;
+}
+
+
+void MainWindow::on_pushButton_clicked()
+{
+     this->close();
+}
+```
+
+`main.cpp`(与 01 例程完全一致):
+
+```cpp
+#include "mainwindow.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    MainWindow w;
+    w.show();
+    return a.exec();
+}
+```
+
+### 7.4 编译及运行(4.1.3)
+
+按 `Ctrl + R` 或点击左下角绿色三角编译运行。点击"关闭程序"按钮,整个程序关闭退出。
+
+此时会浮现两个问题,答案在下一篇:
+
+1. 方法一用 Designer 可视化连接,连接到底写在哪里?——编译后写在生成的 `ui_mainwindow.h` 的 `setupUi()` 里,即 `QObject::connect(pushButton, SIGNAL(clicked()), MainWindow, SLOT(close()));`。
+2. 方法二只是写了个 `on_pushButton_clicked()` 槽函数,为什么会被自动触发?——Qt 通过 **moc 元对象系统 + 命名约定**自动连接,这就是 Qt 信号槽机制。
+
+---
+
+## 8. 跨平台对比
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+| 维度 | Ubuntu(本教程) | Windows | I.MX6ULL(嵌入式) |
+| ---- | --------------- | ------- | ------------------ |
+| 安装方式 | `.run` 安装到 `/opt` | 官方 installer,选 MinGW | 交叉编译工具链 + 目标板 Qt 库 |
+| 编译器 | `gcc`/`g++` | MinGW 的 `g++` 或 MSVC | `arm-linux-gnueabihf-g++` |
+| Qt 版本 | Qt5.12.9 LTS | Qt5.12.9 | 与目标板根文件系统 Qt 版本一致 |
+| 基类选择 | `QMainWindow` 或 `QWidget` | 同 | 常用 `QWidget`(无标题栏/状态栏) |
+| `.pro` 写法 | 三端基本一致 | 三端基本一致 | 只需切换 qmake/Kit |
+| 运行方式 | `./app` | 双击 exe | 拷贝到开发板 `./app` |
+| 显示后端 | X11/Wayland | Windows 原生 | LinuxFB / EGLFS(无桌面) |
+
+结论:**`.pro`/`.ui`/`.h`/`.cpp` 工程结构在三端完全通用**,跨平台的关键只是切换 Kit(编译器 + qmake + Qt 库)。嵌入式的特殊点在于常无窗口系统、常用 `QWidget` 基类。
+
+---
+
+## 9. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+### Q1:Qt 和 Qt Creator 有什么区别?Qt Designer 又是什么?
+
+**答**:Qt 是跨平台的 C++ 开发库,提供界面与非界面(网络、数据库、多媒体等)能力;Qt Creator 是官方 IDE,用来编写、构建、调试项目。Qt Designer(UI 设计器)是 Qt Creator 的一个功能模块,用于可视化拖拽设计 `.ui` 界面文件。三者层次不同:库、IDE、IDE 内的设计器。
+
+### Q2:一个 Qt Widgets 项目里各个文件的作用分别是什么?
+
+**答**:`.pro` 是工程文件,记录 Qt 模块、源文件/头文件/界面文件和编译选项,qmake 据此生成 Makefile;`.ui` 是 Qt Designer 生成的界面 XML,由 uic 转成 `ui_*.h`;`.h` 放类声明与信号槽声明;`.cpp` 放类实现;`main.cpp` 创建 `QApplication` 与主窗口并调用 `exec()` 进入事件循环。构建时还会生成 `moc_*.cpp`(元对象代码)和 `qrc_*.cpp`(资源)。
+
+### Q3:`main.cpp` 里 `QApplication a(argc, argv); ... return a.exec();` 各自的作用?
+
+**答**:每个 Qt 应用必须有且通常只有一个 `QApplication` 对象,它管理应用级资源(字体、光标等)并处理命令行参数。创建主窗口并 `show()` 后,`a.exec()` 把控制交给 Qt,进入**事件循环**,不断接收并分发用户和系统事件到相应窗口部件;当应用退出时 `exec()` 返回其状态码并结束 `main()`。
+
+### Q4:`.ui` 文件和 `ui_*.h` 有什么关系?信号槽连接写在哪里?
+
+**答**:`.ui` 是 XML 格式的界面描述文件,不能手动编辑内容,由 Qt Designer 维护;构建时 uic 工具根据 `.ui` 生成 `ui_*.h`(如 `ui_mainwindow.h`),里面定义 `Ui::MainWindow` 类并在 `setupUi()` 中创建控件。用 Designer 可视化方式连接的信号槽会以 `QObject::connect(...)` 的形式出现在生成的 `setupUi()` 里,所以运行期连接是真实存在的。
+
+### Q5:为什么嵌入式 Qt 项目常选 `QWidget` 而不是 `QMainWindow` 作基类?
+
+**答**:`QMainWindow` 自带主菜单栏、工具栏、状态栏等桌面式主窗口结构,而 `QWidget` 是更基础的可视界面类(`QMainWindow` 本身也由 `QWidget` 继承封装而来)。嵌入式设备通常全屏显示、没有窗口边框和系统标题栏,也不需要菜单/状态栏,因此用更轻量、更自由的 `QWidget` 作为基类更合适。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Qt开发指南V1.2》第三章 初识Qt、第四章 使用Qt Designer开发,以及正点原子 Qt 开发指南例程源码 `Embedded-Qt-Tutorial/Qt/01/01_hello_world`、`02_designer_example`。

+ 714 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/03-信号与槽.md

@@ -0,0 +1,714 @@
+---
+title: 信号与槽
+tags: [嵌入式Linux, Qt, 信号与槽, signal, slot, connect, Q_OBJECT, moc, 元对象系统, QtCreator, 快捷键, IMX6ULL]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第五章 Qt信号与槽、第六章 Qt Creator的使用技巧;例程 Embedded-Qt-Tutorial/Qt/01/03_signal_slot_example"
+---
+
+# 信号与槽
+
+> 💡 **关联知识**:[[05-Qt应用开发/02-Qt入门与项目结构]](`Q_OBJECT`、`ui_mainwindow.h`、Qt Designer 的可视化连接都在这一篇收口)、[[05-Qt应用开发/04-Qt控件详解]](每个控件都有大量内置信号与槽);延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/4. 信号和槽机制]]
+
+信号与槽(Signal & Slot)是 Qt 编程的基础,也是 Qt 的一大创新。有了它,界面各组件之间的交互变得直观而简单。本篇从机制、自定义信号、自定义槽、连接、内置信号槽讲到 Qt Creator 的快捷键与帮助文档,并给出一个完整可运行的自定义信号槽例程。
+
+本章起,教程**不再使用 Qt Designer 开发**,新建项目时取消勾选 `*.ui` 文件。原因是:控件少时 Designer 方便直观,但控件一多就不好管理,每次加控件都要打开 `.ui`;而且信号槽连接藏在生成的 `setupUi()` 里或设计器的连接模式里,**不如代码直观**。统一用代码画界面,能锻炼布局能力和代码逻辑能力。
+
+---
+
+## 1. Qt 信号与槽机制(5.1)
+
+### 1.1 信号与槽是什么
+
+| 概念 | 定义 | 例子 |
+| ---- | ---- | ---- |
+| **信号(Signal)** | 在特定情况下被**发射**的事件 | PushButton 被鼠标单击时发射 `clicked()`;ComboBox 列表项变化时发射 `CurrentIndexChanged()` |
+| **槽(Slot)** | 对信号响应的**函数** | 与信号关联后,信号发射时自动执行 |
+
+槽与普通 C++ 函数的相同点:可以定义在类的任何部分(`public`、`private`、`protected`),可以有任意参数,也可以被直接调用。不同点:**槽函数可以与一个信号关联,当信号被发射时,关联的槽函数被自动执行**。
+
+GUI 程序设计的主要内容,就是对界面上各组件信号的响应——只需知道什么情况下发射哪些信号,合理地去响应和处理即可。
+
+### 1.2 connect 基本格式
+
+信号与槽关联用 `QObject::connect()` 函数实现:
+
+```cpp
+QObject::connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
+```
+
+`connect()` 是 `QObject` 类的**静态函数**,而 `QObject` 是所有 Qt 类的基类,实际调用时可以忽略前面的限定符:
+
+```cpp
+connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));
+```
+
+| 参数 | 含义 |
+| ---- | ---- |
+| `sender` | 发射信号的**对象名称** |
+| `SIGNAL(signal())` | 信号名称,信号可看作特殊函数,**需要带括号**,有参数时要写参数 |
+| `receiver` | 接收信号的**对象名称** |
+| `SLOT(slot())` | 槽函数名称,**需要带括号**,有参数时要写参数 |
+
+`SIGNAL` 和 `SLOT` 是 Qt 的**宏**,用于指明信号和槽,并把它们的参数转换为相应的字符串。
+
+在 Qt Designer 例程 `02_designer_example` 编译生成的 `ui_mainwindow.h`(位于 build 目录)里,`setupUi()` 中有这样一句:
+
+```cpp
+QObject::connect(pushButton, SIGNAL(clicked()), MainWindow, SLOT(close()));
+```
+
+它的作用就是把 `pushButton` 的 `clicked()` 信号与窗体 `MainWindow` 的 `close()` 槽关联,所以单击按钮就会执行窗口的 `close()`。**这解答了上一篇"可视化连接藏在哪里"的疑问**——它在 UI 文件编译后生成的代码里。
+
+### 1.3 信号与槽的规则
+
+| 规则 | 说明 | 示例 |
+| ---- | ---- | ---- |
+| 一个信号可连接**多个槽** | 槽按连接建立的顺序依次执行 | `connect(btn, SIGNAL(clicked()), this, SLOT(hide()));` 与 `connect(btn, SIGNAL(clicked()), this, SLOT(close()));` |
+| 多个信号可连接**同一个槽** | 任一信号发射都执行该槽 | 三个 pushButton 的 `clicked()` 都连到 `close()` |
+| 一个信号可连接**另外一个信号** | 信号发射时也发射另一个信号 | `connect(btn, SIGNAL(objectNameChanged(QString)), this, SIGNAL(windowTitleChanged(QString)));` |
+| 参数匹配 | 严格情况下信号与槽的参数**个数和类型需一致**,至少**信号的参数不能少于槽的参数**;不匹配会编译错误或运行错误 | 有参数的信号连接无参槽通常可以,反之不行 |
+| 必须加 `Q_OBJECT` | 使用信号与槽的类,类定义中**必须**加入宏 `Q_OBJECT`(特别重要) | 见下方 `mainwindow.h` |
+| 执行时机 | 信号发射时,关联槽通常被**立即执行**,像正常调用函数;所有槽执行完,才执行发射信号处后面的代码 | 顺序执行 |
+
+"一个信号连多个槽"的补充:如果两个槽一个是 `hide()`、一个是 `close()`,由于 `close()` 写在后面且执行后程序退出,`hide()` 其实已无意义——但它确实先执行了,因为连接在前。
+
+"一个信号连接另外一个信号"意味着 **connect 万物皆可连**,非常好用。
+
+### 1.4 信号槽的发送与接收方向
+
+```mermaid
+flowchart LR
+    subgraph 发送方
+        S["sender 对象"] --> SIG["signal() 信号"]
+    end
+    subgraph 接收方
+        R["receiver 对象"] --> SL["slot() 槽 / signal() 信号"]
+    end
+    SIG -->|connect| SL
+```
+
+要点:**发送者与发送的信号在一起,接收者与接收的槽/信号在一起,顺序不能在 `connect()` 里写乱。**
+
+### 1.5 断开连接 disconnect
+
+初学者基本用不到断开连接,用到时用 `disconnect()`,它被重载了好几个函数,典型原型:
+
+```cpp
+bool QObject::disconnect(const QObject *sender, const char *signal,
+                         const QObject *receiver, const char *method)
+```
+
+| 需求 | 静态函数写法 | 非静态重载写法 |
+| ---- | ------------ | -------------- |
+| 断开与 `myObject` 连接的一切信号或槽 | `disconnect(myObject, 0, 0, 0);` | `myObject->disconnect();` |
+| 断开所有连接到特定信号的东西 | `disconnect(myObject, SIGNAL(mySignal()), 0, 0);` | `myObject->disconnect(SIGNAL(mySignal()));` |
+| 与指定的接收者断开连接 | `disconnect(myObject, 0, myReceiver, 0);` | `myObject->disconnect(myReceiver);` |
+
+其中 `0` 表示"任意匹配"(C 风格的 NULL)。
+
+### 1.6 connect 的返回值
+
+`connect()` 返回一个 `QMetaObject::Connection` 对象,可用于判断连接是否成功、或稍后断开设定的这一条连接:
+
+```cpp
+QMetaObject::Connection c =
+        connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
+if (c) {
+    /* 连接成功 */
+}
+disconnect(c);   // 只断开这一条连接
+```
+
+相比 `disconnect(myObject, 0, 0, 0)` 这种"一刀切"式断开,保存并断开具体的 `Connection` 更精确。
+
+### 1.7 信号槽 vs 回调函数 vs 事件
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+| 对比项 | 信号与槽 | 回调函数(函数指针) | Qt 事件(event) |
+| ------ | -------- | -------------------- | ---------------- |
+| 耦合度 | 发送者只负责 `emit`,不关心谁接收,**低耦合** | 调用者必须持有并注册回调指针,耦合较高 | 由 `QApplication` 分发到目标对象 |
+| 连接数量 | 一个信号可连多个槽;多个信号可连同一槽 | 通常一个回调只能挂一个函数 | 一个事件到达一个对象 |
+| 类型安全 | 函数指针写法编译期检查;字符串写法运行期检查 | 编译期检查 | 需重写 `event()` 或具体 `xxxEvent()` |
+| 跨线程 | 有 `QueuedConnection` 自动支持 | 需自行加锁/投递 | 需自行 `postEvent` |
+| 典型场景 | 控件交互、模块解耦 | 标准库/底层库回调 | 重绘、鼠标键盘、自定义事件 |
+
+信号槽本质上是 Qt 在"回调"之上加了一层元对象系统,换来**松耦合 + 多对多 + 跨线程**三个好处,代价是 `moc` 生成代码和轻微的性能开销。
+
+---
+
+## 2. 元对象系统与 moc(原理)
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+原书第五章强调"使用信号槽的类必须加 `Q_OBJECT`"并提到了 `ui_mainwindow.h` 等生成文件,但未展开 moc 的内部原理。
+
+### 2.1 为什么信号只需声明、无需定义
+
+信号在类里用 `signals:` 关键字声明,**只写函数原型,不写函数体**。这是因为信号不是由你实现的,而是由 Qt 的 **moc(Meta-Object Compiler,元对象编译器)** 根据声明自动生成实现代码。
+
+流程如下:
+
+1. qmake 扫描工程;
+2. 对包含 `Q_OBJECT` 宏的头文件调用 **moc**,生成 `moc_*.cpp`;
+3. `moc_*.cpp` 与你的 `*.cpp` 一起编译链接。
+
+```mermaid
+flowchart LR
+    H["mainwindow.h\nclass MainWindow : QObject { Q_OBJECT; signals: ... }"] -->|moc| M["moc_mainwindow.cpp\n元对象表 + 信号实现 + qt_metacall"]
+    M --> C["与 mainwindow.cpp 一起编译"]
+    C --> BIN["可执行程序"]
+```
+
+### 2.2 moc 生成的核心内容
+
+| 生成内容 | 作用 |
+| -------- | ---- |
+| `staticMetaObject` | 类的元对象,保存类名、信号槽签名、父类信息等 |
+| 信号的实现函数 | 调用 `QMetaObject::activate()` 发射信号 |
+| `qt_metacall` 等 | 按索引分发信号与槽调用 |
+| `metaObject()` 覆写 | 让运行时能查询类型信息 |
+
+因此 Qt 能在运行时通过**字符串签名**(`SIGNAL(...)`/`SLOT(...)` 宏最终转为字符串)查找并调用对应函数,这就是信号槽跨对象、跨线程也能工作的基础。
+
+### 2.3 Q_OBJECT 与其它 Qt 扩展
+
+- `Q_OBJECT`:启用元对象系统,位置必须在类定义的**第一行**。
+- `signals:`:声明信号区,映射为 `public`,只能声明不能定义。
+- `slots:` / `public slots:` / `private slots:`:声明槽区,本质是普通函数,加宏后可被 connect 按字符串查找。
+- `emit`:发射信号的关键字,其实是空宏,仅用于**语义标记**;也可以省略不写,但不建议。
+- `Q_OBJECT` 缺失是新手最常见的错误之一,症状往往是 connect 编译通过但运行时不触发,或报 `undefined reference to vtable`。
+
+---
+
+## 3. 如何在项目里创建信号(5.2)
+
+新建项目 `03_signal_slot_example`,步骤同 3.6,**取消勾选 `*.ui` 文件**(原因见本章开头)。
+
+**信号只需声明,无需定义**,所以只需在 `mainwindow.h` 里声明即可。下面创建一个 `void pushButtonTextChanged();` 信号——命名最好贴合信号本身的含义,这个信号表示"按钮文本发生改变后"。
+
+`mainwindow.h`(添加信号后):
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QPushButton */
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+signals:
+    /* 声明一个信号,只需声明,无需定义 */
+    void pushButtonTextChanged();
+
+};
+#endif // MAINWINDOW_H
+```
+
+说明(第 16~18 行):声明一个信号 `void pushButtonTextChanged();`,可以看到**信号无需 `public` 等关键字修饰**。
+
+---
+
+## 4. 如何在项目中创建槽(5.3)
+
+创建槽的方法:在 `mainwindow.h` 里**声明**槽,在 `mainwindow.cpp` 里**实现**槽。声明槽必须写定义(函数体实现),否则编译会报错。
+
+槽的特点:
+
+1. 槽可以是任何成员函数、普通全局函数、静态函数;
+2. 槽函数和信号的**参数和返回值要一致**。
+
+由于第 5.2 节声明的信号 `void pushButtonTextChanged();` 无返回值、无参数,所以声明的槽函数也必须 `void` 且无参数。此外再声明一个 `QPushButton` 对象 `pushButton`(可简写 `btn`,按习惯即可,只要不让人看不懂),以及一个处理按钮点击的槽。
+
+`mainwindow.h`(添加槽函数后):
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QPushButton */
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+
+signals:
+    /* 声明一个信号,只需声明,无需定义 */
+    void pushButtonTextChanged();
+
+public slots:
+    /* 声明一个槽函数 */
+    void changeButtonText();
+
+    /* 声明按钮点击的槽函数 */
+    void pushButtonClicked();
+
+private:
+    /* 声明一个对象pushButton */
+    QPushButton *pushButton;
+};
+#endif // MAINWINDOW_H
+```
+
+说明:
+
+- 第 21 行 `public slots:`:声明一个槽函数 `void changeButtonText();`。
+- 第 26 行:声明槽函数 `void pushButtonClicked();`。
+- 第 30 行:声明一个 `QPushButton` 对象 `pushButton`。
+
+在 `mainwindow.cpp` 里实现这两个槽,并实例化 `pushButton`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置窗体的宽为800,高为480 */
+    this->resize(800,480);
+
+    /* 实例化pushButton对象 */
+    pushButton = new QPushButton(this);
+
+    /* 调用setText()方法设定按钮的文本 */
+    pushButton->setText("我是一个按钮");
+}
+
+MainWindow::~MainWindow()
+{
+
+}
+
+/* 实现按钮点击槽函数 */
+void MainWindow::pushButtonClicked()
+{
+    /* 使用emit发送信号 */
+    emit pushButtonTextChanged();
+}
+
+/* 实现按钮文本改变的槽函数 */
+void MainWindow::changeButtonText()
+{
+    /* 在槽函数里改变按钮的文本 */
+   pushButton->setText("被点击了!");
+}
+```
+
+逐行解释:
+
+- 第 7 行:设置程序窗体大小为 800×480,不设置则为最小分辨率。800×480 正是正点原子某款 RGB LCD 屏的分辨率,本教程统一按 **800×480** 开发;实际开发请以硬件实际分辨率为准,不建议低于该分辨率(显示内容过少)。
+- 第 10 行:在堆中实例化 `pushButton`,并指定父对象为 `this`。因为头文件里声明的是指针类型,所以用 `new`。`QPushButton(this)` 的原型其实是 `QPushButton(QWidget *parent = nullptr)`。
+- 第 13 行:`setText()` 设定按钮文本。
+- 第 26~30 行:实现按钮点击槽函数,用 `emit` 发送自定义信号 `pushButtonTextChanged()`。
+- 第 33~37 行:实现按钮文本改变的槽函数,把按钮文本改为"被点击了!"。
+
+`QPushButton` 的构造函数有多个重载(C++ 重载在 Qt 中的体现):
+
+```cpp
+QPushButton(QWidget *parent = nullptr)
+QPushButton(const QString &text, QWidget *parent = nullptr)
+QPushButton(const QIcon &icon, const QString &text, QWidget *parent = nullptr)
+```
+
+---
+
+## 5. 如何在项目中连接信号与槽(5.4)
+
+第 5.2、5.3 节只是声明了信号和定义了槽,要真正生效还需 **connect**:
+
+```cpp
+connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
+connect(this, SIGNAL(pushButtonTextChanged()), this, SLOT(changeButtonText()));
+```
+
+**如何判断发送者和接收者该写谁?**
+
+- `pushButtonClicked()` 是本类定义的槽,`changeButtonText()` 也是本类的槽 → 接收者写 `this`。
+- `pushButtonTextChanged()` 是本类定义的信号 → 发送者也写 `this`。
+- 很多初学者被"信号的发送者和槽的接收者究竟写谁"绕住,细读信号槽定义和上面两条即可理清。
+
+### 5.1 完整可运行例程
+
+`03_signal_slot_example.pro`(注意 `FORMS +=` 后为空,因为不使用 ui):
+
+```pro
+QT       += core gui
+
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+
+CONFIG += c++11
+
+DEFINES += QT_DEPRECATED_WARNINGS
+
+SOURCES += \
+    main.cpp \
+    mainwindow.cpp
+
+HEADERS += \
+    mainwindow.h
+
+FORMS +=
+
+# Default rules for deployment.
+qnx: target.path = /tmp/$${TARGET}/bin
+else: unix:!android: target.path = /opt/$${TARGET}/bin
+!isEmpty(target.path): INSTALLS += target
+```
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QPushButton */
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+signals:
+    /* 声明一个信号,只需声明,无需定义 */
+    void pushButtonTextChanged();
+
+public slots:
+    /* 声明一个槽函数 */
+    void changeButtonText();
+
+    /* 声明按钮点击的槽函数 */
+    void pushButtonClicked();
+
+private:
+    /* 声明一个对象pushButton */
+    QPushButton *pushButton;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置窗体的宽为800,高为480 */
+    this->resize(800,480);
+
+    /* 实例化pushButton对象 */
+    pushButton = new QPushButton(this);
+
+    /* 调用setText()方法设定按钮的文本 */
+    pushButton->setText("我是一个按钮");
+
+    /* 信号与槽连接 */
+    connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
+    connect(this, SIGNAL(pushButtonTextChanged()), this, SLOT(changeButtonText()));
+}
+
+MainWindow::~MainWindow()
+{
+
+}
+
+/* 实现按钮点击槽函数 */
+void MainWindow::pushButtonClicked()
+{
+    /* 使用emit发送信号 */
+    emit pushButtonTextChanged();
+}
+
+/* 实现按钮文本改变的槽函数 */
+void MainWindow::changeButtonText()
+{
+    /* 在槽函数里改变按钮的文本 */
+   pushButton->setText("被点击了!");
+}
+```
+
+`main.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    MainWindow w;
+    w.show();
+    return a.exec();
+}
+```
+
+### 5.2 执行流程
+
+第 16、17 行完成连接后,整个流程如下:
+
+```mermaid
+sequenceDiagram
+    participant U as 用户
+    participant B as pushButton
+    participant M as MainWindow
+    U->>B: 单击按钮
+    B->>M: clicked() 信号
+    M->>M: pushButtonClicked() 槽执行
+    M->>M: emit pushButtonTextChanged()
+    M->>M: changeButtonText() 槽响应
+    M->>B: setText("被点击了!")
+    B-->>U: 按钮文本改变
+```
+
+即:点击按钮 → 触发 `pushButtonClicked()` → 槽里发送 `pushButtonTextChanged()` 信号 → `changeButtonText()` 槽响应 → 按钮文本由"我是一个按钮"变成"被点击了!"。
+
+### 5.3 构建运行
+
+```bash
+cd 03_signal_slot_example
+/opt/Qt5.12.9/5.12.9/gcc_64/bin/qmake 03_signal_slot_example.pro
+make
+./03_signal_slot_example
+```
+
+或在 Qt Creator 中直接 `Ctrl + R`。程序虽简单,但完整实现了**自定义信号与自定义槽**。
+
+---
+
+## 6. 学会使用 Qt 类的信号与槽(5.5)
+
+Qt 里有大量自定义好的信号与槽,基本够用;找不到想要的信号与槽时,再按 5.2~5.4 节自定义。
+
+**怎么知道某个类有哪些信号与槽?**
+
+1. **代码跳转**:在 `connect` 语句里按住 `Ctrl` 再点击 `clicked()`,跳转到该信号的定义处。例如进入 `QPushButton` 的定义,可看到它不止 `clicked`,还有很多其他信号和槽函数。返回上一步用 `Alt + 左方向键`。
+2. **帮助文档**:实际开发中更常用。Qt 帮助文档里能看到类的 Properties、Public Functions、Public Slots、Signals 等,是最权威的资料(下一节详述)。
+
+---
+
+## 7. connect 的其它写法与连接类型
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+原书第五章只讲解 Qt4/字符串式 `SIGNAL`/`SLOT` 写法,并声明"Qt4 与 Qt5 写法有区别、本教程对 Qt4 写法不做讲解"。本节介绍的**函数指针写法、lambda 槽、连接类型**为 Qt5 扩展知识。
+
+### 7.1 两种连接写法对比
+
+**写法一:字符串签名(原书用法,继续有效)**
+
+```cpp
+connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
+```
+
+- 编译期不检查信号/槽是否存在,拼写错误要到**运行时**才在控制台报 "No such signal/slot"。
+- 参数用字符串描述,宏展开后作为元对象查找的 key。
+
+**写法二:函数指针(Qt5 推荐,编译期检查)**
+
+```cpp
+connect(pushButton, &QPushButton::clicked, this, &MainWindow::pushButtonClicked);
+```
+
+- 编译期即可发现拼写错误、参数不匹配,更安全。
+- 不需要 `SIGNAL`/`SLOT` 宏,不需要写参数类型字符串。
+- 重载信号需要 `qOverload` 或 `static_cast` 消歧,例如 `&QComboBox::currentIndexChanged` 有重载版本。
+- 不能用于"信号连信号"中目标也是信号的情况?——可以,函数指针同样支持信号连信号。
+
+| 对比项 | `SIGNAL/SLOT` 字符串写法 | 函数指针写法 |
+| ------ | ------------------------ | ------------ |
+| 错误检查时机 | 运行期 | 编译期 |
+| 是否需要 `SIGNAL`/`SLOT` | 需要 | 不需要 |
+| 重载信号 | 字符串自动区分 | 需 `qOverload`/`static_cast` |
+| 可否给槽传 lambda | 否 | 可以 |
+| Qt5/Qt6 支持 | 支持(保留) | 推荐 |
+
+### 7.2 槽可以是 lambda 表达式
+
+函数指针写法下,接收者可以省略,直接用 lambda 作为槽:
+
+```cpp
+connect(pushButton, &QPushButton::clicked, this, [=]() {
+    pushButton->setText("被点击了!");
+});
+```
+
+- `[=]` 按值捕获外部变量,`[this]` 捕获当前对象指针。
+- lambda 槽无法用 `SIGNAL/SLOT` 字符串写法连接。
+- 简洁,非常适合一次性、简单的响应逻辑。
+
+### 7.3 连接类型 Qt::ConnectionType
+
+`connect` 的第 5 个可选参数控制投递方式:
+
+```cpp
+connect(sender, SIGNAL(sig()), receiver, SLOT(slot()), Qt::ConnectionType type);
+```
+
+| 枚举值 | 行为 | 线程关系要求 |
+| ------ | ---- | ------------ |
+| `Qt::AutoConnection`(默认) | 发送者与接收者同线程→Direct;跨线程→Queued | 通用 |
+| `Qt::DirectConnection` | 立即在**发送者所在线程**直接调用槽 | 任意 |
+| `Qt::QueuedConnection` | 把调用封装为事件投递到**接收者线程**的事件队列,异步执行 | 接收者线程须有事件循环 |
+| `Qt::BlockingQueuedConnection` | 同 Queued,但发送者阻塞直到槽执行完 | **同线程使用会死锁** |
+| `Qt::UniqueConnection` | 与上述之一按位或使用,防止**重复连接**同一对信号槽 | 通用 |
+
+```mermaid
+flowchart TD
+    SIG["信号发射"] --> SAME{"发送者与接收者\n是否同线程?"}
+    SAME -->|同线程| D["DirectConnection\n立即调用"]
+    SAME -->|跨线程| Q["QueuedConnection\n投递到接收者事件队列"]
+    Q --> EV["接收者事件循环取出\n在接收者线程执行槽"]
+```
+
+要点:默认 `AutoConnection` 已能覆盖绝大多数场景;跨线程时 Qt 自动用队列连接,因此**不要手动 new 线程对象后直接在错误线程调用其函数**。`UniqueConnection` 常用来避免同一连接被建立多次导致槽被重复执行。
+
+---
+
+## 8. Qt Creator 的使用技巧(第六章)
+
+### 8.1 快捷键(6.1)
+
+想知道某个按钮的快捷键,把鼠标移到按钮上即可看到提示(需在英文输入法下)。
+
+**一般快捷键**
+
+| 功能 | 快捷键 |
+| ---- | ------ |
+| 新建文件或项目(N) | `Ctrl + N` |
+| 关闭当前窗口/文件 | `Ctrl + W` |
+| 关闭所有文件 | `Ctrl + Shift + W` |
+| 关闭当前文件(Windows) | `Ctrl + F4` |
+| **运行** | `Ctrl + R` |
+| 返回上一级(常用于跳转代码) | `Alt + ←` |
+| 进入下一级(常用于跳转代码) | `Alt + →` |
+| Qt 自动排版对齐代码 | `Ctrl + I` |
+| 减小字体 | `Ctrl + -`(或 `Ctrl + 滚轮向下`) |
+| 增大字体 | `Ctrl + +`(或 `Ctrl + 滚轮向上`) |
+| 重置字体大小 | `Ctrl + 0` |
+| 折叠 | `Ctrl + <` |
+
+**常用编辑快捷键**
+
+| 功能 | 快捷键 |
+| ---- | ------ |
+| 展开 | `Ctrl + >` |
+| 复制行 | `Ctrl + Ins` |
+| 复制到行下 | `Ctrl + Alt + Down` |
+| 复制到行上 | `Ctrl + Alt + Up` |
+| 在当前行上方插入新行 | `Ctrl + Shift + Enter` |
+| 在当前行下方插入新行 | `Ctrl + Enter` |
+| 查看剪切板历史 | `Ctrl + Shift + V` |
+| 剪切行 | `Shift + Del` |
+| 追加行 | `Ctrl + J` |
+| 向下移动当前行 | `Ctrl + Shift + Down` |
+| 向上移动当前行 | `Ctrl + Shift + Up` |
+| 切换函数声明/定义 | `Ctrl + 鼠标左键` / `Shift + F2` |
+| **编辑信号和槽** | `F4` |
+| 跳转至以 `}` 结尾的块 | `Ctrl + }` |
+| 跳转至以 `{` 开始的块 | `Ctrl + {` |
+| 打开类型层次窗口 | `Ctrl + Shift + T` |
+
+快捷键可能与 Ubuntu/Windows 系统冲突。可在"工具 → 选项 → 环境 → 键盘"里修改:选中命令,按"Record"重新记录新组合键,再 Apply / OK。**注意不要与系统快捷键重复**(例如 `Ctrl + M` 显示红色即表示与系统冲突)。没有定义快捷键的命令,也可以按同样步骤自定义。
+
+### 8.2 Qt 帮助文档的使用(6.2)
+
+Qt 帮助文档在安装时已随 Qt 一起安装,位于"安装目录 / Qt5.12.9 / Docs /"下(html 文本,也可用浏览器打开);Qt Creator 内置了同一套文档。
+
+**在 Qt Creator 中查文档的步骤**:
+
+1. 点击左侧"帮助"。
+2. 把检索方式下拉选为"索引(Index)"。
+3. 输入关键字,如 `QPushButton`,也可只输入 `QPush` 匹配。
+4. 点击匹配项进入文档页。
+
+**如何阅读一个类的文档页**:
+
+| 标题 | 内容 |
+| ---- | ---- |
+| Properties | 类有哪些属性 |
+| Public Functions | 类有哪些公开函数(方法) |
+| **Public Slots** | 类有哪些可用槽 |
+| Signals | 类有哪些信号 |
+| Detailed Description | 详细说明与示例 |
+
+查看某个方法的用法,直接单击进入即可。记得用 `Alt + ←` / `Alt + →` 在上下级文档间跳转。
+
+Qt 帮助文档是**最权威、最全面**的中文/英文资料,任何 Qt 教程都没有它详细(尽管是英文);Qt 还自带大量示例可以参考。看文档要多练,**并不是看会了就会了,实践起来才知道**。
+
+```mermaid
+flowchart LR
+    A["不知道 QPushButton 有什么信号槽"] --> B["帮助 → 索引 → 输入 QPush"]
+    B --> C["进入类文档页"]
+    C --> D["Properties / Public Functions"]
+    C --> E["Signals / Public Slots"]
+    E --> F["Ctrl 点击 connect 中的函数名直接跳转"]
+```
+
+---
+
+## 9. 常见错误排查
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+| 现象 | 可能原因 | 排查手段 |
+| ---- | -------- | -------- |
+| 点击按钮没反应 | 漏写 `connect`,或连接写反了发送者/接收者 | 检查 `connect(sender, SIGNAL(...), receiver, SLOT(...))` 顺序;确认在 `ui->setupUi(this)` 之后连接 |
+| 编译报 `undefined reference to vtable for ...` | 类里写了 `Q_OBJECT` 却没重新运行 qmake/moc | Qt Creator 里执行"构建 → 执行 qmake",再重新构建;确认 `.pro` 里 `HEADERS` 包含该头文件 |
+| 运行时报 `QObject::connect: No such signal/slot` | `SIGNAL`/`SLOT` 字符串拼写错误、参数不一致、忘写括号 | 用 `Ctrl` 点击跳转核对签名;优先改用函数指针写法让编译期报错 |
+| 槽执行了两次 | 同一对信号槽被 connect 了多次 | 用 `Qt::UniqueConnection`,或检查是否重复初始化 |
+| 跨线程槽里操作界面崩溃 | 在子线程直接调用了 UI 对象 | 用信号槽(默认 `AutoConnection`)把结果投递回主线程再更新界面 |
+| 自定义信号编译报 `error: no matching function` | 信号只声明未定义?其实正常 | 信号本就不该写函数体;检查是否误把信号写成普通函数、或忘加 `signals:` 区 |
+| 中文字符显示乱码 | 源文件编码或未配置中文输入 | 见 [[05-Qt应用开发/02-Qt入门与项目结构]] 的中文环境/输入法配置 |
+
+---
+
+## 10. 面试精选
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+### Q1:信号与槽是什么?它们和普通函数有什么关系?
+
+**答**:信号是在特定情况下被发射的事件(如按钮被点击发射 `clicked()`);槽是对信号响应的函数。槽本身就是普通 C++ 函数,可以定义在 `public`/`protected`/`private` 下、可以有任意参数、也可以被直接调用;它的特殊之处仅在于能与信号关联,信号发射时被自动执行。信号则只声明不定义,由 moc 生成实现。
+
+### Q2:为什么使用信号与槽的类必须加 `Q_OBJECT` 宏?信号为什么只声明不实现?
+
+**答**:`Q_OBJECT` 宏启用 Qt 的元对象系统,moc 依据它扫描并生成 `moc_*.cpp`,其中包含元对象表、信号的实现(内部调用 `QMetaObject::activate`)以及按索引分发调用的 `qt_metacall` 等。正因为信号由 moc 自动实现,类里只写函数原型即可,无需(也不能)自己写函数体。缺少 `Q_OBJECT` 会导致信号槽无法工作,常见报错是 `undefined reference to vtable` 或运行时 "No such signal/slot"。
+
+### Q3:`SIGNAL/SLOT` 宏写法与函数指针写法有什么区别?
+
+**答**:`SIGNAL`/`SLOT` 宏把函数签名转成字符串,connect 在运行期按字符串查找匹配,编译期不检查,拼写错误要到运行时才发现;函数指针写法(如 `connect(btn, &QPushButton::clicked, this, &MainWindow::slot)`)在编译期就能检查信号/槽是否存在、参数是否匹配,更安全,也能连接 lambda 作为槽;重载信号时函数指针需要 `qOverload` 或 `static_cast` 消歧。
+
+### Q4:信号与槽的参数、返回值有什么要求?一个信号能连接几个槽?连接顺序如何?
+
+**答**:槽函数的参数和返回值要与信号一致;严格情况下二者参数个数和类型需匹配,至少信号的参数不能少于槽的参数,否则会编译或运行错误。一个信号可以连接多个槽,也可以连接另一个信号;多个信号也可以连接同一个槽。一个信号连多个槽时,槽按连接建立时的顺序依次执行,并且所有槽执行完才继续执行发射信号处后面的代码(默认直接调用情形)。
+
+### Q5:跨线程使用信号槽时要注意什么?`Qt::AutoConnection` 的行为是什么?
+
+**答**:`connect` 的第五个参数可指定连接类型。默认 `Qt::AutoConnection` 会判断发送者与接收者是否在同一线程:同线程按 `DirectConnection` 立即在发送者线程调用槽;跨线程按 `QueuedConnection` 把调用封装成事件投递到接收者线程的事件队列,由接收者的事件循环取出执行,从而保证槽在接收者线程执行,避免竞态。`BlockingQueuedConnection` 会阻塞发送者直到槽执行完,但**不能用于同一线程,否则死锁**;`UniqueConnection` 与其它类型按位或,可防止重复连接。
+
+---
+
+**内容来源**:《I.MX6U嵌入式Qt开发指南V1.2》第五章 Qt信号与槽、第六章 Qt Creator的使用技巧,以及正点原子 Qt 开发指南例程源码 `Embedded-Qt-Tutorial/Qt/01/03_signal_slot_example`。

+ 3003 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/04-Qt控件详解.md

@@ -0,0 +1,3003 @@
+---
+title: Qt控件详解
+tags: [嵌入式Linux, Qt, Qt Widgets, 控件, QPushButton, QToolButton, QRadioButton, QCheckBox, QCommandLinkButton, QDialogButtonBox, QComboBox, QFontComboBox, QLineEdit, QTextEdit, QPlainTextEdit, QSpinBox, QDoubleSpinBox, QTimeEdit, QDateEdit, QDateTimeEdit, QDial, QScrollBar, QSlider, QKeySequenceEdit, QLabel, QCalendarWidget, QLCDNumber, QProgressBar, QFrame, QTextBrowser, QGraphicsView, I.MX6U]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第七章 Qt控件 7.1 按钮、7.2 输入窗口部件、7.3 显示窗口部件、7.4 显示窗口部件之浏览器;例程 Embedded-Qt-Tutorial/Qt/01/04_qpushbutton~28_qgraphicsview"
+---
+
+# Qt控件详解
+
+> 💡 **关联知识**:[[05-Qt应用开发/03-信号与槽]]、[[05-Qt应用开发/05-布局容器与模型视图]];延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/8. 常用控件]]
+
+本篇覆盖《I.MX6U嵌入式Qt开发指南》第七章的前半部分:**按钮类**(7.1)、**输入窗口部件**(7.2)、**显示窗口部件**(7.3)、**浏览器类**(7.4),共 27 个控件。每个控件都按“控件简介 → 常用属性与信号 → 用法示例(完整源码)→ 运行效果”组织,代码取自配套例程 `04_qpushbutton` ~ `28_qgraphicsview`,去掉版权后可直接编译。
+
+---
+
+## 0. 全章总览
+
+### 0.1 控件继承关系
+
+Qt 的控件几乎都从 `QWidget` 派生。理解继承关系,就能明白一个控件为什么能使用父类的属性和信号。
+
+```mermaid
+classDiagram
+    class QObject
+    class QWidget
+    class QAbstractButton
+    class QPushButton
+    class QToolButton
+    class QRadioButton
+    class QCheckBox
+    class QCommandLinkButton
+    class QDialogButtonBox
+    class QAbstractSpinBox
+    class QSpinBox
+    class QDoubleSpinBox
+    class QDateTimeEdit
+    class QTimeEdit
+    class QDateEdit
+    class QAbstractSlider
+    class QDial
+    class QScrollBar
+    class QSlider
+    class QFrame
+    class QLabel
+    class QLCDNumber
+    class QProgressBar
+    class QAbstractScrollArea
+    class QTextEdit
+    class QPlainTextEdit
+    class QTextBrowser
+    class QGraphicsView
+
+    QObject <|-- QWidget
+    QWidget <|-- QAbstractButton
+    QAbstractButton <|-- QPushButton
+    QAbstractButton <|-- QToolButton
+    QAbstractButton <|-- QRadioButton
+    QAbstractButton <|-- QCheckBox
+    QPushButton <|-- QCommandLinkButton
+    QWidget <|-- QDialogButtonBox
+    QWidget <|-- QAbstractSpinBox
+    QAbstractSpinBox <|-- QSpinBox
+    QAbstractSpinBox <|-- QDoubleSpinBox
+    QAbstractSpinBox <|-- QDateTimeEdit
+    QDateTimeEdit <|-- QTimeEdit
+    QDateTimeEdit <|-- QDateEdit
+    QWidget <|-- QAbstractSlider
+    QAbstractSlider <|-- QDial
+    QAbstractSlider <|-- QScrollBar
+    QAbstractSlider <|-- QSlider
+    QWidget <|-- QFrame
+    QFrame <|-- QLabel
+    QFrame <|-- QLCDNumber
+    QWidget <|-- QProgressBar
+    QWidget <|-- QAbstractScrollArea
+    QAbstractScrollArea <|-- QTextEdit
+    QTextEdit <|-- QPlainTextEdit
+    QTextEdit <|-- QTextBrowser
+    QAbstractScrollArea <|-- QGraphicsView
+```
+
+### 0.2 本章控件索引
+
+| 分组 | 控件 | 一句话作用 | 例程 |
+| ---- | ---- | ---------- | ---- |
+| 按钮 | `QPushButton` | 下压按钮,执行命令或触发事件 | 04 |
+| 按钮 | `QToolButton` | 工具按钮,通常显示图标,用于工具栏 | 05 |
+| 按钮 | `QRadioButton` | 单选按钮,一组内多选一 | 06 |
+| 按钮 | `QCheckBox` | 复选按钮,可多选多,支持三态 | 07 |
+| 按钮 | `QCommandLinkButton` | 命令链接按钮,带描述文本与箭头 | 08 |
+| 按钮 | `QDialogButtonBox` | 对话框标准按钮盒 | 09 |
+| 输入 | `QComboBox` | 下拉组合框 | 10 |
+| 输入 | `QFontComboBox` | 字体组合框 | 11 |
+| 输入 | `QLineEdit` | 单行文本编辑框 | 12 |
+| 输入 | `QTextEdit` | 富文本编辑框 | 13 |
+| 输入 | `QPlainTextEdit` | 纯文本编辑框 | 14 |
+| 输入 | `QSpinBox` | 整数微调框 | 15 |
+| 输入 | `QDoubleSpinBox` | 浮点微调框 | 16 |
+| 输入 | `QTimeEdit` / `QDateEdit` / `QDateTimeEdit` | 时间/日期/日期时间编辑 | 17 |
+| 输入 | `QDial` | 圆形范围控制(拨盘) | 18 |
+| 输入 | `QScrollBar` | 滚动条 | 19 |
+| 输入 | `QSlider` | 滑动条 | 20 |
+| 输入 | `QKeySequenceEdit` | 快捷键序列录入 | 21 |
+| 显示 | `QLabel` | 显示文本或图像 | 22 |
+| 显示 | `QCalendarWidget` | 月历,选择日期 | 23 |
+| 显示 | `QLCDNumber` | LCD 风格数字显示 | 24 |
+| 显示 | `QProgressBar` | 进度条 | 25 |
+| 显示 | `QFrame` | 框架基类,可画水平/垂直线 | 26 |
+| 浏览器 | `QTextBrowser` | 带超文本导航的文本浏览器 | 27 |
+| 浏览器 | `QGraphicsView` | 图形视图框架,可视化场景 | 28 |
+
+---
+
+## 1. 按钮类
+
+Qt 内置六种按钮部件,都直接或间接继承 `QAbstractButton`:
+
+- **QPushButton**:下压按钮,继承 `QAbstractButton`,被 `QCommandLinkButton` 继承,通常用于执行命令或触发事件。
+- **QToolButton**:工具按钮,继承 `QAbstractButton`,通常放在工具栏,显示图标,支持自动浮起。
+- **QRadioButton**:单选按钮,继承 `QAbstractButton`,成组出现,提供“多选一”。
+- **QCheckBox**:复选按钮,继承 `QAbstractButton`,提供“多选多”。
+- **QCommandLinkButton**:命令链接按钮,继承 `QPushButton`,除文字外默认带一个箭头图标,表明按下会打开另一窗口或页面。
+- **QDialogButtonBox**:按钮盒,继承 `QWidget`,用于对话框里的“确定”“取消”等标准按钮。
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+>
+> `QAbstractButton` 是所有按钮的公共基类,它提供按钮共有的一组属性与信号(下方各控件小节中会用到):
+>
+> | 成员 | 类型 | 说明 |
+> | ---- | ---- | ---- |
+> | `setText(QString)` / `text()` | 属性 | 按钮显示文本 |
+> | `setIcon(QIcon)` / `icon()` | 属性 | 按钮图标 |
+> | `setCheckable(bool)` | 属性 | 是否可切换选中状态 |
+> | `setChecked(bool)` / `isChecked()` | 属性 | 选中状态 |
+> | `setEnabled(bool)` | 属性 | 是否可用 |
+> | `clicked()` | 信号 | 被点击(最常用) |
+> | `pressed()` / `released()` | 信号 | 按下 / 释放 |
+> | `toggled(bool)` | 信号 | 可选中按钮的状态变化 |
+> | `setShortcut(QKeySequence)` | 方法 | 设置快捷键 |
+
+### 1.1 QPushButton
+
+#### 控件简介
+
+`QPushButton` 继承 `QAbstractButton`,是最常用的按钮。通常用于执行命令或触发事件。第 4 章已在 Qt Designer 里用过它连接信号槽关闭程序,这里改用纯代码实现。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QPushButton(const QString &text, QWidget *parent)` | 构造时直接传入显示文本与父对象 |
+| `setGeometry(x, y, w, h)` | 设置位置与大小 |
+| `setText(QString)` | 设置文本 |
+| `setStyleSheet(QString)` | 设置样式表,例如背景色 |
+| `clicked()` | 点击信号 |
+
+#### 用法示例
+
+**例 04_qpushbutton 窗口换肤**:单击不同按钮改变窗口背景色。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QPushButton类 */
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QPushButton对象pushButton1 */
+    QPushButton *pushButton1;
+    /* 声明一个QPushButton对象pushButton2 */
+    QPushButton *pushButton2;
+
+private slots:
+    /* 声明对象pushButton1的槽函数 */
+    void pushButton1_Clicked();
+    /* 声明对象pushButton2的槽函数 */
+    void pushButton2_Clicked();
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置宽高为800×480,位置在0, 0。(0, 0)代表原点,Qt默认最左上角的点为原点 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化两个按钮对象,并设置其显示文本为窗口皮肤1和窗口皮肤2 */
+    pushButton1 = new QPushButton("窗口皮肤1", this);
+    pushButton2 = new QPushButton("窗口皮肤2", this);
+
+    /* 设定两个QPushButton对象的位置 */
+    pushButton1->setGeometry(300, 200, 80, 40);
+    pushButton2->setGeometry(400, 200, 80, 40);
+
+    /* 信号槽连接 */
+    connect(pushButton1, SIGNAL(clicked()), this, SLOT(pushButton1_Clicked()));
+    connect(pushButton2, SIGNAL(clicked()), this, SLOT(pushButton2_Clicked()));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+/* 槽函数的实现 */
+void MainWindow::pushButton1_Clicked()
+{
+    /* 设置主窗口的样式1 */
+    this->setStyleSheet("QMainWindow { background-color: rgba(255, 245, 238, 100%); }");
+}
+
+/* 槽函数的实现 */
+void MainWindow::pushButton2_Clicked()
+{
+    /* 设置主窗口的样式2 */
+    this->setStyleSheet("QMainWindow { background-color: rgba(238, 122, 233, 100%); }");
+}
+```
+
+> 按钮文本不宜设置过小,否则文字可能显示不全。`background-color` 使用 `rgba(r, g, b, a)`,其中 `a` 是百分比不透明度。
+
+#### 运行效果说明
+
+编译运行后,点击“窗口皮肤 1”主窗体变为浅色,点击“窗口皮肤 2”变为紫色。窗口默认大小为 800×480。
+
+---
+
+### 1.2 QToolButton
+
+#### 控件简介
+
+`QToolButton`(工具按钮)区别于 `QPushButton` 的一点是**可以带图标**。图标与按钮背景图片不是一回事:图标是按钮内容,背景图是按钮底图。工具栏(`QToolBar`)上放带图标和文本的按钮时,`QToolButton` 是首选。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setIcon(QIcon)` | 设置图标 |
+| `setText(QString)` | 设置文本 |
+| `setToolButtonStyle(Qt::ToolButtonStyle)` | 图标与文本的排布方式 |
+| `clicked()` | 点击信号 |
+
+常用的 `Qt::ToolButtonStyle`:`Qt::ToolButtonIconOnly`(只显示图标)、`Qt::ToolButtonTextOnly`(只显示文本)、`Qt::ToolButtonTextBesideIcon`(文本在图标旁)、`Qt::ToolButtonTextUnderIcon`(文本在图标下)、`Qt::ToolButtonFollowStyle`。
+
+#### 用法示例
+
+**例 05_qtoolbutton 自定义工具栏**:把系统标准图标做成工具按钮嵌入工具栏。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QToolButton类 */
+#include <QToolButton>
+/* 引入QToolBar类 */
+#include <QToolBar>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QToolButton对象 */
+    QToolButton *toolButton;
+    /* 声明一个QToolBar对象 */
+    QToolBar *toolBar;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include <QApplication>
+#include <QStyle>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体的位置和大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化QToolBar对象 */
+    toolBar = new QToolBar(this);
+    /* 设置toolBar的位置和大小 */
+    toolBar->setGeometry(0, 0, 800, 100);
+
+    /* 实例化QStyle类对象,用于设置风格,调用系统类自带的图标 */
+    QStyle *style = QApplication::style();
+
+    /* 使用Qt自带的标准图标,可以在帮助文档里搜索QStyle::StandardPixmap */
+    QIcon icon = style->standardIcon(QStyle::SP_TitleBarContextHelpButton);
+
+    /* 实例化QToolButton对象 */
+    toolButton = new QToolButton();
+
+    /* 设置图标 */
+    toolButton->setIcon(icon);
+    /* 设置要显示的文本 */
+    toolButton->setText("帮助");
+    /* 设置toolButton的样式,文本置于图标下方 */
+    toolButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
+
+    /* 最后将toolButton添加到ToolBar里 */
+    toolBar->addWidget(toolButton);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+运行后工具栏左上角出现一个“帮助”工具按钮,图标在上、文本在下。这种设计在许多带工具栏的软件中都能见到。
+
+---
+
+### 1.3 QRadioButton
+
+#### 控件简介
+
+`QRadioButton` 提供带文本标签的单选框,可在选中(checked)与未选中(unchecked)间切换。同一父对象下默认一次只能选中一个,即“多选一”。典型场景是视频网站切换线路。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setText(QString)` | 设置文本 |
+| `setChecked(bool)` / `isChecked()` | 设置/读取选中状态 |
+| `toggled(bool)` | 选中状态变化信号 |
+| `clicked()` | 点击信号 |
+
+#### 用法示例
+
+**例 06_radiobutton 仿手机开关**:用 qss 样式表把单选框画成开关图片。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QRadioButton */
+#include <QRadioButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明两个QRadioButton对象 */
+    QRadioButton *radioButton1;
+    QRadioButton *radioButton2;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 主窗体设置位置和显示的大小 */
+    this->setGeometry(0, 0, 800, 480);
+    this->setStyleSheet("QMainWindow {background-color: rgba(200, 50, 100, 100%);}");
+
+    /* 实例化对象 */
+    radioButton1 = new QRadioButton(this);
+    radioButton2 = new QRadioButton(this);
+
+    /* 设置两个QRadioButton的位置和显示大小 */
+    radioButton1->setGeometry(300, 200, 100, 50);
+    radioButton2->setGeometry(400, 200, 100, 50);
+
+    /* 设置两个QRadioButton的显示文本 */
+    radioButton1->setText("开关一");
+    radioButton2->setText("开关二");
+
+    /* 设置初始状态,radioButton1的Checked为false,另一个为true */
+    radioButton1->setChecked(false);
+    radioButton2->setChecked(true);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+`main.cpp`(引入资源里的 qss 并设为全局样式):
+
+```cpp
+#include "mainwindow.h"
+
+#include <QApplication>
+/* 引入QFile */
+#include <QFile>
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);
+    /* 指定文件 */
+    QFile file(":/style.qss");
+
+    /* 判断文件是否存在 */
+    if (file.exists() ) {
+        /* 以只读的方式打开 */
+        file.open(QFile::ReadOnly);
+        /* 以字符串的方式保存读出的结果 */
+        QString styleSheet = QLatin1String(file.readAll());
+        /* 设置全局样式 */
+        qApp->setStyleSheet(styleSheet);
+        /* 关闭文件 */
+        file.close();
+    }
+    MainWindow w;
+    w.show();
+    return a.exec();
+}
+```
+
+`style.qss`:
+
+```css
+QRadioButton{
+    spacing: 2px;
+    color: white;
+}
+QRadioButton::indicator {
+    width: 45px;
+    height: 30px;
+}
+QRadioButton::indicator:unchecked {
+    image: url(:/images/switch_off.png);
+}
+QRadioButton::indicator:checked {
+    image: url(:/images/switch_on.png);
+}
+```
+
+> **资源文件添加步骤**(后续例程通用):右键项目 → `Add New…` → Qt → `Qt Resource Files` → 命名(如 `res`);在 `res.qrc` 中 `Add Prefix` 填 `/`(必写,类似 Linux 根节点);再 `Add Files` 添加 `images/` 下的图片(`images` 文件夹需先手动创建);`Ctrl+S` 保存后生效。qss 文件则通过 `Add New…` → 选择 qss 模板创建。
+
+#### 运行效果说明
+
+点击“开关一”,开关二自动关闭;点“开关二”,开关一则打开——同组互斥。因为 indicator 使用了开/关两张图片,看上去就像手机开关。
+
+---
+
+### 1.4 QCheckBox
+
+#### 控件简介
+
+`QCheckBox` 继承 `QAbstractButton`。与 `QRadioButton` 的区别是选择模式:单选提供“多选一”,复选提供“多选多”。开启三态后还有“半选”状态,常见于安装程序。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setText(QString)` | 设置文本 |
+| `setTristate(bool)` | 开启三态(不开启只有选中/未选中) |
+| `setCheckState(Qt::CheckState)` | 设置状态 |
+| `checkState()` | 读取状态 |
+| `stateChanged(int)` | 状态变化信号(返回 `Qt::Checked` / `Qt::Unchecked` / `Qt::PartiallyChecked`) |
+
+#### 用法示例
+
+**例 07_qcheckbox 三态选择框**:点击复选框循环显示三种状态文本。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QCheckBox */
+#include <QCheckBox>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QCheckBox对象 */
+    QCheckBox *checkBox;
+private slots:
+    /* 声明QCheckBox的槽函数,并带参数传递,用这个参数接收信号的参数 */
+    void checkBoxStateChanged(int);
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 主窗体设置位置和显示的大小及背景颜色 */
+    this->setGeometry(0, 0, 800, 480);
+    this->setStyleSheet("QMainWindow {background-color: rgba(100, 100, 100, 100%);}");
+
+    /* 实例化对象 */
+    checkBox = new QCheckBox(this);
+
+    /* 设置QCheckBox位置和显示大小 */
+    checkBox->setGeometry(350, 200, 250, 50);
+
+    /* 初始化三态复选框的状态为Checked */
+    checkBox->setCheckState(Qt::Checked);
+
+    /* 设置显示的文本 */
+    checkBox->setText("初始化为Checked状态");
+
+    /* 开启三态模式,必须开启,否则只有两种状态,即Checked和Unchecked */
+    checkBox->setTristate();
+
+    /* 连接checkBox的信号stateChanged(int),与我们定义的槽checkBoxStateChanged(int)连接 */
+    connect(checkBox, SIGNAL(stateChanged(int)), this, SLOT(checkBoxStateChanged(int)));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+/* 槽函数的实现 */
+void MainWindow::checkBoxStateChanged(int state)
+{
+    /* 判断checkBox的state状态,设置checkBox的文本 */
+    switch (state) {
+    case Qt::Checked:
+        /* 选中状态 */
+        checkBox->setText("Checked状态");
+        break;
+    case Qt::Unchecked:
+        /* 未选中状态 */
+        checkBox->setText("Unchecked状态");
+        break;
+    case Qt::PartiallyChecked:
+        /* 半选状态 */
+        checkBox->setText("PartiallyChecked状态");
+        break;
+    default:
+        break;
+    }
+}
+```
+
+`style.qss`:
+
+```css
+QCheckBox{
+    spacing: 5px;
+    color: white;
+}
+QCheckBox::indicator {
+    width: 50px;
+    height: 50px;
+}
+QCheckBox::indicator:enabled:unchecked {
+    image: url(:/images/unchecked.png);
+}
+QCheckBox::indicator:enabled:checked {
+    image: url(:/images/checked.png);
+}
+QCheckBox::indicator:enabled:indeterminate {
+    image: url(:/images/indeterminate.png);
+}
+```
+
+`main.cpp` 与 1.3 节相同(加载 qss 并设为全局样式)。
+
+#### 运行效果说明
+
+多次点击复选框,文本在 `Checked`、`PartiallyChecked`、`Unchecked` 三种状态间循环,indicator 显示对应图片。
+
+---
+
+### 1.5 QCommandLinkButton
+
+#### 控件简介
+
+`QCommandLinkButton`(命令链接按钮)继承 `QPushButton`。与 `QRadioButton` 相似,用于在互斥选项中选择一项;表面上同平面按钮,但默认带一个箭头图标,表示按下会打开另一个窗口或页面。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QCommandLinkButton(const QString &text, const QString &description, QWidget *parent)` | 构造:文本 + 描述 + 父对象 |
+| `setDescription(QString)` | 设置描述文本 |
+| `setIcon(QIcon)` | 设置图标 |
+| `clicked()` | 点击信号 |
+
+#### 用法示例
+
+**例 08_qcommandlinkbutton 链接窗口**:点击按钮调用系统窗口打开 `/home` 目录。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QCommandLinkButton */
+#include <QCommandLinkButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QCommandLinkButton对象 */
+    QCommandLinkButton *commandLinkButton;
+
+private slots:
+    /* 声明槽函数,用于点击commandLinkButton后触发 */
+    void commandLinkButtonClicked();
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+/* 引入桌面服务,用来打开系统文件夹对话框 */
+#include <QDesktopServices>
+/* 引入QUrl */
+#include <QUrl>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 主窗体设置位置和显示的大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化对象,原型为:
+     * QCommandLinkButton(const QString &text, const QString &description, QWidget *parent) */
+    commandLinkButton = new QCommandLinkButton(
+                "打开/home目录", "点击此将调用系统的窗口打开/home目录",this);
+
+    /* 设置QCommandLinkButton位置和显示大小 */
+    commandLinkButton->setGeometry(300, 200, 250, 60);
+
+    /* 信号槽连接 */
+    connect(commandLinkButton, SIGNAL(clicked()), this,
+            SLOT(commandLinkButtonClicked()));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::commandLinkButtonClicked()
+{
+    /* 调用系统服务打开/home目录 */
+    QDesktopServices::openUrl(QUrl("file:////home/") );
+}
+```
+
+#### 运行效果说明
+
+点击“打开 `/home` 目录”按钮,系统弹出文件管理窗口,定位到 `/home` 目录。
+
+---
+
+### 1.6 QDialogButtonBox
+
+#### 控件简介
+
+不同平台的对话框按钮布局不同。`QDialogButtonBox` 允许开发人员添加按钮,并自动使用适合当前桌面环境的布局。既可以使用系统自带的标准按钮,也可以自定义按钮。
+
+标准按钮(`QDialogButtonBox::StandardButton`)包括:`Ok`、`Cancel`、`Open`、`Save`、`Close`、`Discard`、`Apply`、`Reset`、`RestoreDefaults`、`Help`、`SaveAll` 等。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addButton(StandardButton)` | 添加标准按钮 |
+| `addButton(QAbstractButton*, ButtonRole)` | 添加自定义按钮并指定角色 |
+| `button(StandardButton)` | 取得某个标准按钮的指针 |
+| `clicked(QAbstractButton*)` | 按钮点击信号,可判断点了哪个 |
+
+#### 用法示例
+
+**例 09_qdialogbuttonbox**:同时使用标准按钮“取消”和自定义按钮“自定义”。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QDialogButtonBox */
+#include <QDialogButtonBox>
+/* 引入QPushButton */
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QDialogButtonBox对象 */
+    QDialogButtonBox *dialogButtonBox;
+
+    /* 声明一个QPushButton对象 */
+    QPushButton *pushButton;
+
+private slots:
+    /* 声明信号槽,带QAbstractButton *参数,用于判断点击了哪个按钮 */
+    void dialogButtonBoxClicked(QAbstractButton *);
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+/* 引入QDebug */
+#include <QDebug>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 主窗体设置位置和显示的大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化并设置按钮的盒子的大小和位置 */
+    dialogButtonBox = new  QDialogButtonBox(this);
+    dialogButtonBox->setGeometry(300, 200, 200, 30);
+
+    /* 使用Qt的Cancel按钮 */
+    dialogButtonBox->addButton(QDialogButtonBox::Cancel);
+
+    /* 将英文"Cancel"按钮设置为中文"取消" */
+    dialogButtonBox->button(QDialogButtonBox::Cancel)->setText("取消");
+
+    /* 实例化自定义按钮 */
+    pushButton = new QPushButton(tr("自定义"));
+
+    /* 将pushButton添加到dialogButtonBox,并设定ButtonRole为ActionRole */
+    dialogButtonBox->addButton(pushButton, QDialogButtonBox::ActionRole);
+
+    /* 信号槽连接,带参数QAbstractButton *,用于判断用户点击哪个按键 */
+    connect(dialogButtonBox, SIGNAL(clicked(QAbstractButton * )),
+            this, SLOT(dialogButtonBoxClicked(QAbstractButton *)));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::dialogButtonBoxClicked(QAbstractButton *button)
+{
+    /* 判断点击的对象是否为QDialogButtonBox::Cancel */
+    if(button == dialogButtonBox->button(QDialogButtonBox::Cancel)) {
+        /* 打印“单击了取消键” */
+        qDebug() <<"单击了取消键"<<endl;
+    /* 判断点击的对象是否为pushButton */
+    }else if(button == pushButton) {
+        /* 打印“单击了自定义键” */
+        qDebug() <<"单击了自定义键"<<endl;
+    }
+}
+```
+
+> `addButton` 的重载原型为 `void QDialogButtonBox::addButton(QAbstractButton *button, QDialogButtonBox::ButtonRole role)`。`qDebug()` 是 Qt 的调试打印,功能与 C++ 的 `cout` 类似,输出到“应用程序输出”窗口。
+
+#### 运行效果说明
+
+点击“自定义”与“取消”按钮,应用程序输出窗口分别打印“单击了自定义键”“单击了取消键”。
+
+---
+
+## 2. 输入窗口部件
+
+Qt Designer 的输入部件面板提供 16 种输入部件,涵盖组合框、编辑框、微调框、拨盘、滚动条、滑动条、快捷键录入等。下面按例程逐个讲解。
+
+### 2.1 QComboBox
+
+#### 控件简介
+
+`QComboBox` 继承 `QWidget`,被 `QFontComboBox` 继承。它以占用最少屏幕空间的方式向用户展示选项列表,是最常用的下拉控件。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addItem(QString)` | 添加一项 |
+| `itemText(int index)` | 取第 index 项的文本 |
+| `currentIndex()` | 当前索引 |
+| `count()` | 项数 |
+| `currentIndexChanged(int)` | 当前项改变信号 |
+
+#### 用法示例
+
+**例 10_qcombobox 选择省份**:选项变化时打印省份名。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QComboBox */
+#include <QComboBox>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QComboBox对象 */
+    QComboBox *comboBox;
+
+private slots:
+    /* 声明QComboBox对象的槽函数 */
+     void comboBoxIndexChanged(int);
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+/* 引入QDebug */
+#include <QDebug>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体的显示位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化对象 */
+    comboBox = new QComboBox(this);
+
+    /* 设置comboBox的显示位置与大小 */
+    comboBox->setGeometry(300, 200, 150, 30);
+
+    /* 添加项,我们添加三个省份,作为comboBox的三个选项 */
+    comboBox->addItem("广东(默认)");
+    comboBox->addItem("湖南");
+    comboBox->addItem("四川");
+
+    /* 信号槽连接 */
+    connect(comboBox, SIGNAL(currentIndexChanged(int)), this,
+            SLOT(comboBoxIndexChanged(int)));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::comboBoxIndexChanged(int index)
+{
+    /* 打印出选择的省份 */
+    qDebug()<<"您选择的省份是"<< comboBox->itemText(index)<<endl;
+}
+```
+
+#### 运行效果说明
+
+点击下拉框选择“湖南”,输出窗口打印“您选择的省份是湖南”。多账号登录等场景常用此控件。
+
+---
+
+### 2.2 QFontComboBox
+
+#### 控件简介
+
+`QFontComboBox` 继承 `QComboBox`,是选择字体系列的组合框。列表按字母顺序填充系统字体家族名,常用于工具栏中配合字号、粗体、斜体按钮使用。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `currentFontChanged(QFont)` | 当前字体变化信号 |
+| `itemText(int index)` | 取第 index 项名称 |
+| `currentIndex()` | 当前索引 |
+
+#### 用法示例
+
+**例 11_qfontcombobox 字体选择**:选择字体后,标签立即用该字体显示。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+/* 引入QFontComboBox */
+#include <QFontComboBox>
+/* 引入QLabel */
+#include <QLabel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QFontComboBox对象 */
+    QFontComboBox *fontComboBox;
+    /* 声明一个Label对象,用于显示当前字体变化 */
+    QLabel *label;
+
+private slots:
+    /* 声明QFontComboBox对象使用的槽函数 */
+    void fontComboBoxFontChanged(QFont);
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体的显示位置和大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化对象 */
+    fontComboBox = new QFontComboBox(this);
+    label = new QLabel(this);
+
+    /* 设置显示的位置与大小 */
+    fontComboBox->setGeometry(280, 200, 200, 30);
+    label->setGeometry(280, 250, 300, 50);
+
+    /* 信号与槽连接 */
+    connect(fontComboBox, SIGNAL(currentFontChanged(QFont)), this,
+            SLOT(fontComboBoxFontChanged(QFont)));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+/* 槽函数实现 */
+void MainWindow::fontComboBoxFontChanged(QFont font)
+{
+    /* 将label里的文本内容设置为所选择的字体 */
+    label->setFont(font);
+
+    /* 定义一个字符串接收当前项的字体 */
+    QString str = "用此标签显示字体效果\n设置的字体为:" +
+            fontComboBox->itemText(fontComboBox->currentIndex());
+
+    /* 将字符串的内容作为label的显示内容 */
+    label->setText(str);
+}
+```
+
+#### 运行效果说明
+
+选择字体后,标签文字立即变成该字体,并显示字体名称。Ubuntu 与 Windows 字体不同,效果会有差异。
+
+---
+
+### 2.3 QLineEdit
+
+#### 控件简介
+
+`QLineEdit` 是单行文本编辑器,允许输入和编辑一行纯文本,支持撤销/重做、剪切/粘贴、拖放。通过 `setEchoMode()` 还可作为“只写”字段输入密码。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `text()` | 读取文本 |
+| `setText(QString)` | 设置文本 |
+| `clear()` | 清空 |
+| `setEchoMode(QLineEdit::EchoMode)` | 回显模式(如 `Password`) |
+| `textChanged(QString)` / `returnPressed()` | 文本变化 / 回车信号 |
+
+#### 用法示例
+
+**例 12_qlineedit 单行输入框**:点击“确认”把输入内容显示到标签,并清空输入框。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QLineEdit>
+#include <QPushButton>
+#include <QLabel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+private:
+    /* 声明一个QLineEdit对象 */
+    QLineEdit *lineEdit;
+
+    /* 声明一个QPushButton对象 */
+    QPushButton *pushButton;
+
+    /* 声明一个QLabel对象 */
+    QLabel *label;
+
+private slots:
+    /* 声明一个槽函数,响应pushButton的clicked事件 */
+    void pushButtonClicked();
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    this->setGeometry(0, 0, 800, 480);
+
+    lineEdit = new QLineEdit(this);
+    lineEdit->setGeometry(280, 200, 200, 20);
+
+    pushButton = new QPushButton(this);
+    pushButton->setGeometry(500, 200, 50, 20);
+    pushButton->setText("确认");
+
+    label = new QLabel(this);
+    label->setGeometry(280, 250, 400, 20);
+    label->setText("你输入的内容是:");
+
+    /* 信号槽连接 */
+    connect(pushButton,SIGNAL(clicked()), this,
+            SLOT(pushButtonClicked()));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::pushButtonClicked()
+{
+    /* 字符串变量str */
+    QString str;
+
+    str = "你输入的内容是:";
+    str += lineEdit->text();
+
+    /* 设置label里的文本显示内容 */
+    label->setText(str);
+    /* 在点击了确认键之后清空lineEdit单行输入框 */
+    lineEdit->clear();
+}
+```
+
+#### 运行效果说明
+
+输入文本后点击“确认”,标签显示输入内容,输入框被清空,可继续输入。
+
+---
+
+### 2.4 QTextEdit
+
+#### 控件简介
+
+`QTextEdit` 继承 `QAbstractScrollArea`,被 `QTextBrowser` 继承。它是高级的所见即所得查看器/编辑器,支持 HTML 样式的富文本(字体、颜色、图片、列表、表格),并针对大型文档和快速响应做了优化。内容超出视图时自动出现滚动条。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setText(QString)` / `toPlainText()` | 设置/读取纯文本 |
+| `setHtml(QString)` / `toHtml()` | 设置/读取富文本 |
+| `selectAll()` | 全选 |
+| `clear()` | 清空 |
+| `setFocus()` | 设置焦点 |
+| `textChanged()` | 文本变化信号 |
+
+#### 用法示例
+
+**例 13_qtextedit 文本编辑框**:两个按钮模拟全选与清除。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+#include <QTextEdit>
+#include <QPushButton>
+
+#include <QMainWindow>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QTextEdit对象 */
+    QTextEdit *textEdit;
+
+    /* 声明两个QPushButton对象 */
+    QPushButton *pushButtonSelectAll;
+    QPushButton *pushButtonClearAll;
+
+private slots:
+    /* 声明两个槽函数,响应按钮点击响应的事件 */
+    void pushButtonSelectAllClicked();
+    void pushButtonClearAllClicked();
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体显示的位置和大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化对象,设置位置和显示大小 */
+    textEdit = new QTextEdit(this);
+    textEdit->setGeometry(0, 0, 800, 400);
+
+    /* 全选按钮 */
+    pushButtonSelectAll = new QPushButton(this);
+    pushButtonSelectAll->setGeometry(200, 420, 50, 20);
+    pushButtonSelectAll->setText("全选");
+
+    /* 清除按钮 */
+    pushButtonClearAll = new QPushButton(this);
+    pushButtonClearAll->setGeometry(500, 420, 50, 20);
+    pushButtonClearAll->setText("清除");
+
+    /* 信号槽连接 */
+    connect(pushButtonSelectAll, SIGNAL(clicked()), this,
+            SLOT(pushButtonSelectAllClicked()));
+    connect(pushButtonClearAll, SIGNAL(clicked()), this,
+            SLOT(pushButtonClearAllClicked()));
+
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::pushButtonSelectAllClicked()
+{
+    /* 设置焦点为textEdit */
+    textEdit->setFocus();
+    /* 判断文本编辑框内容是否为空,不为空则全选 */
+    if(!textEdit->toPlainText().isEmpty()){
+        /* 全选 */
+        textEdit->selectAll();
+    }
+}
+
+void MainWindow::pushButtonClearAllClicked()
+{
+    /* 清空textEdit里的文本内容 */
+    textEdit->clear();
+}
+```
+
+#### 运行效果说明
+
+在编辑框输入文字后,点击“全选”文字被选中(必须先 `setFocus()`,否则无法选中);点击“清除”清空全部内容。
+
+---
+
+### 2.5 QPlainTextEdit
+
+#### 控件简介
+
+`QPlainTextEdit` 是支持纯文本的高级查看器/编辑器,可理解为 `QTextEdit` 的低配版:只支持纯文本,不支持富文本,但显示效率更高。需要显示大量文字、频繁滚动时,`QPlainTextEdit` 表现更好。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `insertPlainText(QString)` | 插入纯文本 |
+| `setPlainText(QString)` / `toPlainText()` | 设置/读取纯文本 |
+| `setReadOnly(bool)` | 只读模式 |
+| `textChanged()` | 文本变化信号 |
+
+#### 用法示例
+
+**例 14_qplaintextedit 文本浏览编辑器**:读取当前目录下的 `moc_mainwindow.cpp` 并按只读开关切换。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QPlainTextEdit>
+#include <QRadioButton>
+
+#include <QMainWindow>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QPlainTextEdit *plainTextEdit;
+    QRadioButton *radioButton;
+
+private slots:
+    /* 槽函数 */
+    void radioButtonClicked();
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include <QDir>
+#include <QTextStream>
+#include <QCoreApplication>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置当前程序的工作目录为可执行程序的工作目录 */
+    QDir::setCurrent(QCoreApplication::applicationDirPath());
+
+    this->setGeometry(0, 0, 800, 480);
+
+    plainTextEdit = new QPlainTextEdit(this);
+    plainTextEdit->setGeometry(0, 50, 800, 430);
+
+    radioButton = new QRadioButton(this);
+    radioButton->setGeometry(650, 20, 100, 20);
+    radioButton->setText("只读模式");
+
+    /* 打开可执行程序目录里的moc_mainwindow.cpp,注意如果是Windows下
+       moc_mainwindow.cpp并不在当前目录,而在上一级目录"../moc_mainwindow.cpp" */
+    QFile file("moc_mainwindow.cpp");
+
+    /* 以只读模式打开,但是可以在plainTextEdit里编辑 */
+    file.open((QFile::ReadOnly | QFile::Text));
+
+    /* 加载到文件流 */
+    QTextStream in(&file);
+
+    /* 从文本流中读取全部 */
+    plainTextEdit->insertPlainText(in.readAll());
+
+    /* 信号槽连接 */
+    connect(radioButton, SIGNAL(clicked()), this,
+            SLOT(radioButtonClicked()));
+
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::radioButtonClicked()
+{
+    /* 检查radioButton是否选中 */
+    if(radioButton->isChecked()) {
+        /* 设置为只读模式 */
+        plainTextEdit->setReadOnly(true);
+    } else {
+        /* 设置为非只读模式 */
+        plainTextEdit->setReadOnly(false);
+    }
+}
+```
+
+#### 运行效果说明
+
+程序启动后把可执行目录下的 `moc_mainwindow.cpp` 读入编辑框,此时可编辑;勾选“只读模式”后不可编辑,取消勾选后恢复编辑。
+
+---
+
+### 2.6 QSpinBox
+
+#### 控件简介
+
+`QSpinBox` 继承 `QAbstractSpinBox`,用于处理整数和离散值。用户可通过上下按钮、键盘上下键或手动输入来增减当前值。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setRange(int min, int max)` | 范围 |
+| `setSingleStep(int)` | 步长 |
+| `setValue(int)` / `value()` | 当前值 |
+| `setPrefix(QString)` / `setSuffix(QString)` | 前缀/后缀 |
+| `valueChanged(int)` | 值变化信号 |
+
+#### 用法示例
+
+**例 15_qspinbox 窗口不透明度调节器**:用微调框控制窗体透明度。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QSpinBox>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QSpinBox对象 */
+    QSpinBox *spinBox;
+private slots:
+    /* 槽函数 */
+    void spinBoxValueChanged(int);
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 设置主窗口背景颜色,rgb颜色标准,a代表不透明度(0~100)*/
+    this->setStyleSheet("QMainWindow{background-color: "
+                        "rgba(100, 100, 100, 100%) }");
+
+    spinBox = new QSpinBox(this);
+    spinBox->setGeometry(350, 200, 150, 30);
+
+    /* 设置范围0~100 */
+    spinBox->setRange(0, 100);
+
+    /* 设置步长为10 */
+    spinBox->setSingleStep(10);
+
+    /* 设置初始值为100 */
+    spinBox->setValue(100);
+
+    /* 设置后缀 */
+    spinBox->setSuffix("%不透明度");
+
+    /* 信号槽连接 */
+    connect(spinBox,SIGNAL(valueChanged(int)), this,
+            SLOT(spinBoxValueChanged(int)));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::spinBoxValueChanged(int opacity)
+{
+    /* 转换为double数据类型 */
+    double  dobleopacity = (double)opacity / 100;
+
+    /* 设置窗体不透明度,范围是0.0~1.0。1则为不透明,0为全透明 */
+    this->setWindowOpacity(dobleopacity);
+}
+```
+
+#### 运行效果说明
+
+初始不透明度 100% 即完全不透明;点向下微调后值变小,整个窗口逐渐透明。
+
+---
+
+### 2.7 QDoubleSpinBox
+
+#### 控件简介
+
+`QDoubleSpinBox` 继承 `QAbstractSpinBox`,用于处理浮点值,用法与 `QSpinBox` 基本相同,区别是数据类型为浮点。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setRange(double min, double max)` | 范围 |
+| `setSingleStep(double)` | 步长 |
+| `setValue(double)` / `value()` | 当前值 |
+| `setPrefix(QString)` / `setSuffix(QString)` | 前缀/后缀 |
+| `valueChanged(double)` | 值变化信号 |
+
+#### 用法示例
+
+**例 16_qdoublespinbox 窗口大小调节器**:按百分比缩放整个窗口。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QDoubleSpinBox>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明一个QDoubleSpinBox对象 */
+    QDoubleSpinBox *doubleSpinBox;
+
+private slots:
+    /* 槽函数 */
+    void doubleSpinBoxValueChanged(double);
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化和设置显示的位置与大小 */
+    doubleSpinBox = new QDoubleSpinBox(this);
+    doubleSpinBox->setGeometry((this->width() - 200) / 2,
+                               (this->height() - 30) / 2,
+                               200, 30);
+    /* 设置前缀 */
+    doubleSpinBox->setPrefix("窗口大小");
+
+    /* 设置后缀 */
+    doubleSpinBox->setSuffix("%");
+
+    /* 设置范围 */
+    doubleSpinBox->setRange(50.00, 100.00);
+
+    /* 设置初始值 */
+    doubleSpinBox->setValue(100.00);
+
+    /* 设置步长 */
+    doubleSpinBox->setSingleStep(0.1);
+
+    /* 信号槽连接 */
+    connect(doubleSpinBox, SIGNAL(valueChanged(double)),
+            SLOT(doubleSpinBoxValueChanged(double)));
+
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::doubleSpinBoxValueChanged(double value)
+{
+    /* 重新计算窗口的宽与高 */
+    int w = 800 * value / 100;
+    int h = 480 * value / 100;
+
+    /* 重新设置窗口的宽与高 */
+    this->setGeometry(0, 0, w, h);
+
+    /* 重新设置doubleSpinBox的显示位置 */
+    doubleSpinBox->setGeometry((this->width() - 200) / 2,
+                               (this->height() - 30) / 2,
+                               200, 30);
+
+}
+```
+
+#### 运行效果说明
+
+窗口初始 100%,向下调节时窗口按比例缩小,最小 50.00%;向上调节时放大,最大 100.00%。
+
+---
+
+### 2.8 QTimeEdit / QDateEdit / QDateTimeEdit
+
+#### 控件简介
+
+三者放在同一个例程中讲解:
+
+- `QDateTimeEdit` 用于编辑日期和时间,允许用键盘或方向键增减各部分的值。
+- `QTimeEdit` 继承 `QDateTimeEdit`,专门编辑时间。
+- `QDateEdit` 继承 `QDateTimeEdit`,专门编辑日期。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QDateTimeEdit(const QDateTime&, QWidget*)` | 以日期时间构造 |
+| `QTimeEdit(const QTime&, QWidget*)` | 以时间构造 |
+| `QDateEdit(const QDate&, QWidget*)` | 以日期构造 |
+| `setCalendarPopup(bool)` | 是否弹出日历选择 |
+| `dateTime()` / `time()` / `date()` | 读取值 |
+| `dateTimeChanged(QDateTime)` / `timeChanged(QTime)` / `dateChanged(QDate)` | 值变化信号 |
+
+#### 用法示例
+
+**例 17_qdatetimeedit 时间日期展示**:同时展示三种日期时间控件。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QDateTimeEdit>
+#include <QTimeEdit>
+#include <QDateEdit>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QDateTimeEdit *dateTimeEdit;
+    QTimeEdit *timeEdit;
+    QDateEdit *dateEdit;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化对象,传入当前日期与时间 */
+    dateTimeEdit = new QDateTimeEdit(
+                QDateTime::currentDateTime(),this);
+    dateTimeEdit->setGeometry(300, 200, 200, 30);
+    /* 弹出日期控件与否 */
+    //dateTimeEdit->setCalendarPopup(true);
+
+    /* 实例化对象,传入当前时间 */
+    timeEdit = new QTimeEdit(QTime::currentTime(),this);
+    timeEdit->setGeometry(300, 240, 200, 30);
+
+    /* 实例化对象,传入当前日期 */
+    dateEdit = new QDateEdit(QDate::currentDate(),this);
+    dateEdit->setGeometry(300, 280, 200, 30);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+程序启动后三个控件分别显示系统当前日期时间、时间和日期。Windows 与 Linux 的日期显示格式可能不同。
+
+---
+
+### 2.9 QDial
+
+#### 控件简介
+
+`QDial` 继承 `QAbstractSlider`,提供圆形范围控制(类似速度计或电位器)。适合在环形范围(如 0~359 度)内取值,或布局上需要正方形部件的场合。由于继承自 `QAbstractSlider`,行为与滑块类似,共享相同的信号、槽与成员函数。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setRange(int min, int max)` | 范围 |
+| `setPageStep(int)` | 页长(两个最大刻度间距) |
+| `setNotchesVisible(bool)` | 刻度是否可见 |
+| `setNotchTarget(double)` | 两个凹槽间的目标像素数 |
+| `setWrapping(bool)` | 是否可指向整圈任意角度 |
+| `valueChanged(int)` | 值变化信号 |
+
+#### 用法示例
+
+**例 18_qdial 车速表**:拖动拨盘,标签显示“车速”。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QDial>
+#include <QLabel>
+#include <QMainWindow>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QDial *dial;
+    QLabel *label;
+
+private slots:
+    /* 槽函数 */
+    void dialValueChanged(int);
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体的位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化对象和设置显示位置与大小 */
+    dial = new QDial(this);
+    dial->setGeometry(300, 100, 200, 200);
+
+    /* 设置页长(两个最大刻度的间距)*/
+    dial->setPageStep(10);
+
+    /* 设置刻度可见 */
+    dial->setNotchesVisible(true);
+
+    /* 设置两个凹槽之间的目标像素数 */
+    dial->setNotchTarget(1.00);
+
+    /* 设置dial值的范围 */
+    dial->setRange(0,100);
+
+    /* 开启后可指向圆的任何角度 */
+    //dial->setWrapping(true);
+
+    /* 实例化对象和设置显示位置与大小 */
+    label = new QLabel(this);
+    label->setGeometry(370, 300, 200, 50);
+
+    /* 初始化为0km/h */
+    label->setText("0km/h");
+
+    /* 信号槽连接 */
+    connect(dial, SIGNAL(valueChanged(int)),
+            this, SLOT(dialValueChanged(int)));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::dialValueChanged(int val)
+{
+    /* QString::number()转换成字符串 */
+    label->setText(QString::number(val) + "km/h");
+}
+```
+
+#### 运行效果说明
+
+用鼠标拖动拨盘或用方向键增减,标签实时显示当前“车速”。音量控制、仪表盘等场景常用。
+
+---
+
+### 2.10 QScrollBar
+
+#### 控件简介
+
+`QScrollBar` 继承 `QAbstractSlider`,提供垂直或水平滚动条,让用户访问比显示区域更大的文档。它提供当前位置和可见文档量的可视化指示。一般滚动条不单独使用,而是配合 `QScrollArea`,或由自带滚动条的控件(如 `QListWidget`)内部使用。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QScrollBar(Qt::Orientation, QWidget*)` | 构造:方向 + 父对象 |
+| `setRange(int min, int max)` | 范围 |
+| `setValue(int)` / `value()` | 当前值 |
+| `setPageStep(int)` | 页步长 |
+| `valueChanged(int)` | 值变化信号 |
+
+#### 用法示例
+
+**例 19_qscrollbar 创建水平和垂直滚动条**:
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QScrollBar>
+#include <QLabel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明水平滚动条对象 */
+    QScrollBar *horizontalScrollBar;
+
+    /* 声明垂直滚动条对象 */
+    QScrollBar *verticalScrollBar;
+
+    /* 声明标签文本 */
+    QLabel *label;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体大小,位置 */
+    this->setGeometry(0,0,800,480);
+
+    /* 实例化水平滚动条及设置位置大小 */
+    horizontalScrollBar = new QScrollBar(Qt::Horizontal, this);
+    horizontalScrollBar->setGeometry(0, 450, 800, 30);
+
+    /* 实例化垂直滚动条及设置位置大小 */
+    verticalScrollBar = new QScrollBar(Qt::Vertical, this);
+    verticalScrollBar->setGeometry(770, 0, 30, 480);
+
+    /* 实例化,标签文本 */
+    label = new QLabel(this);
+    /* 设置文本 */
+    label->setText("这是一个测试");
+    /* 设置位置大小 */
+    label->setGeometry(300, 200, 100, 20);
+}
+```
+
+#### 运行效果说明
+
+窗口底部和右侧分别出现水平、垂直滚动条。本例只演示创建实例,不对滚动效果做细化。
+
+---
+
+### 2.11 QSlider
+
+#### 控件简介
+
+`QSlider` 继承 `QAbstractSlider`,提供垂直或水平滑动条。用户沿凹槽移动滑块手柄,位置被转换为合法范围内的整数值。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QSlider(Qt::Orientation, QWidget*)` | 构造:方向 + 父对象 |
+| `setRange(int min, int max)` | 范围 |
+| `setValue(int)` / `value()` | 当前值 |
+| `setSliderPosition(int)` | 设置滑块位置 |
+| `valueChanged(int)` | 值变化信号 |
+| `sliderMoved(int)` | 拖动时信号 |
+
+#### 用法示例
+
+**例 20_qslider 水平与垂直滑动条联动**:
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QSlider>
+#include <QLabel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QSlider *horizontalSlider;
+    QSlider *verticalSlider;
+    QLabel *label;
+private slots:
+    /* 槽函数 */
+    void horizontalSliderValueChanged(int);
+    void verticalSliderValueChanged(int);
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化水平滑动条对象*/
+    horizontalSlider = new QSlider(Qt::Horizontal, this);
+
+    /* 设置显示的位置与大小 */
+    horizontalSlider->setGeometry(250, 100, 200, 20);
+
+    /* 设置值范围 */
+    horizontalSlider->setRange(0, 100);
+
+    /* 实例化垂直滑动条对象 */
+    verticalSlider = new QSlider(Qt::Vertical, this);
+
+    /* 设置显示的位置与大小 */
+    verticalSlider->setGeometry(200, 50, 20, 200);
+
+    /* 设置值范围 */
+    verticalSlider->setRange(0, 100);
+
+    /* 实例化标签文本 */
+    label = new QLabel("滑动条值:0", this);
+    label->setGeometry(250, 200, 100, 20);
+
+    /* 信号槽连接 */
+    connect(horizontalSlider, SIGNAL(valueChanged(int)),
+            this, SLOT(horizontalSliderValueChanged(int)));
+    connect(verticalSlider, SIGNAL(valueChanged(int)),
+            this, SLOT(verticalSliderValueChanged(int)));
+
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::horizontalSliderValueChanged(int val)
+{
+    /* 当水平滑动条的值改变时,改变垂直滑动条的值 */
+    verticalSlider->setSliderPosition(val);
+
+    /* 将int类型转变成字符 */
+    QString str = "滑动条值:" + QString::number(val);
+
+    /* 显示当前垂直或水平滑动条的值 */
+    label->setText(str);
+
+}
+
+void MainWindow::verticalSliderValueChanged(int val)
+{
+    /* 当垂直滑动条的值改变时,改变水平滑动条的值 */
+    horizontalSlider->setSliderPosition(val);
+}
+```
+
+#### 运行效果说明
+
+拖动任一滑动条,另一个同步变化,标签显示当前值。
+
+---
+
+### 2.12 QKeySequenceEdit
+
+#### 控件简介
+
+`QKeySequenceEdit` 继承 `QWidget`,允许用户录入 `QKeySequence`(通常用作快捷键)。当控件获得焦点、用户释放最后一个按键后约一秒结束记录,是录制快捷键的专用控件。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `keySequence()` / `setKeySequence(QKeySequence)` | 读取/设置快捷键序列 |
+| `keySequenceChanged(const QKeySequence &)` | 序列变化信号 |
+| `clear()` | 清空 |
+
+#### 用法示例
+
+**例 21_qkeysequenceedit 自定义快捷键**:录入 Ctrl+Q 则关闭窗口。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QKeySequenceEdit>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明QKeySequenceEdit对象 */
+    QKeySequenceEdit  *keySequenceEdit;
+
+private slots:
+    /* 声明槽 */
+    void KSEKeySequenceChanged(const QKeySequence &);
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include <QDebug>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 主窗体设置位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化 */
+    keySequenceEdit = new QKeySequenceEdit(this);
+
+    /* 设置位置与大小 */
+    keySequenceEdit->setGeometry(350, 200, 150, 30);
+
+    /* 信号槽连接 */
+    connect(keySequenceEdit,
+            SIGNAL(keySequenceChanged(const QKeySequence &)),
+            this,
+            SLOT(KSEKeySequenceChanged(const QKeySequence &))
+            );
+
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::KSEKeySequenceChanged(
+        const QKeySequence &keySequence)
+{
+    /* 判断输入的组合键是否为Ctrl + Q,如果是则退出程序 */
+    if(keySequence == QKeySequence(tr("Ctrl+Q"))) {
+        /* 结束程序 */
+        this->close();
+    }else {
+        /* 打印出按下的组合键 */
+        qDebug()<<keySequence.toString()<<endl;
+    }
+}
+```
+
+#### 运行效果说明
+
+焦点在控件上时按下任意键或组合键,约一秒后控件记录并打印该组合键;按下 Ctrl+Q 时程序窗口关闭。
+
+---
+
+## 3. 显示窗口部件
+
+Qt 显示部件面板提供 Label、Text Browser、Graphics View、Calendar Widget、LCD Number、Progress Bar、Horizontal/Vertical Line、OpenGL Widget、QQuick Widget 等。其中 Text Browser、Graphics View 在下一节“浏览器类”讲解。
+
+### 3.1 QLabel
+
+#### 控件简介
+
+`QLabel` 继承 `QFrame`,用于显示文本或图像。前面例子只用它显示文本,其实它也能显示图片。按钮、Widget、Label 都能显示图像,但按钮有按下、释放、悬停三种状态,不常用于单纯显示一张图。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setText(QString)` | 设置文本 |
+| `setPixmap(QPixmap)` | 设置图像 |
+| `setScaledContents(bool)` | 允许缩放填充 |
+| `setAlignment(Qt::Alignment)` | 对齐方式 |
+| `setFont(QFont)` | 字体 |
+
+#### 用法示例
+
+**例 22_qlbel 标签显示图像或文本**:一个标签显示图片,一个标签显示文字。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QLabel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 用一个QLabel对象用于显示字符串 */
+    QLabel *labelString;
+
+    /* 用一个QLabel对象用于显示图像 */
+    QLabel *labelImage;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置大小与位置 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 使用资源里的文件时格式是  :+前缀+文件路径  */
+    QPixmap pixmap(":images/openedv.png");
+
+    labelImage = new QLabel(this);
+
+    /* 标签大小为452×132,根据图像的大小来设置 */
+    labelImage->setGeometry(180, 150, 452, 132);
+    /* 设置图像 */
+    labelImage->setPixmap(pixmap);
+    /* 开启允许缩放填充 */
+    labelImage->setScaledContents(true);
+
+    labelString = new QLabel(this);
+    labelString->setText("标签演示文本");
+    labelString->setGeometry(300, 300, 100, 20);
+}
+```
+
+#### 运行效果说明
+
+窗口中央显示 `openedv.png` 图片,下方显示文字“标签演示文本”。标签大小按图片实际尺寸 452×132 设置,避免图片被拉伸变形。
+
+---
+
+### 3.2 QCalendarWidget
+
+#### 控件简介
+
+`QCalendarWidget` 继承 `QWidget`,提供基于月的日历,允许用户选择日期。它用当前月份和年份初始化,并提供若干公共槽来切换显示的年份和月份。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `selectedDate()` | 当前选中日期 |
+| `setSelectedDate(QDate)` | 设置选中日期 |
+| `selectionChanged()` | 选择变化信号 |
+| `setFont(QFont)` | 设置字体 |
+| `setGridVisible(bool)` | 是否显示网格 |
+
+#### 用法示例
+
+**例 23_qcalendarwidget 日历简单使用**:选择日期并显示,按钮回到当前日期。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QCalendarWidget>
+#include <QPushButton>
+#include <QLabel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明QCalendarWidget,QPushButton,QLabel对象 */
+    QCalendarWidget *calendarWidget;
+    QPushButton *pushButton;
+    QLabel *label;
+
+private slots:
+    /* 槽函数 */
+    void calendarWidgetSelectionChanged();
+    void pushButtonClicked();
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置位置与大小,下同 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 对象实例化设置显示的位置与大小 */
+    calendarWidget = new QCalendarWidget(this);
+    calendarWidget->setGeometry(200, 20, 400, 300);
+
+    QFont font;
+    /* 设置日历里字体的大小为10像素 */
+    font.setPixelSize(10);
+    calendarWidget->setFont(font);
+
+    /* 回到当前日期按钮 */
+    pushButton = new QPushButton("回到当前日期",this);
+    pushButton->setGeometry(200, 350, 100, 30);
+
+    /* 标签显示当前选择日期 */
+    label = new QLabel(this);
+    label->setGeometry(400, 350, 400, 30);
+    QString str = "当前选择的日期:"
+            + calendarWidget->selectedDate().toString();
+    label->setText(str);
+
+    /* 信号槽连接 */
+    connect(calendarWidget, SIGNAL(selectionChanged()),
+            this, SLOT(calendarWidgetSelectionChanged()));
+    connect(pushButton, SIGNAL(clicked()),
+            this, SLOT(pushButtonClicked()));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::calendarWidgetSelectionChanged()
+{
+    /* 当日历点击改变当前选择的日期时,更新Label的显示内容 */
+    QString str = "当前选择的日期:"
+            + calendarWidget->selectedDate().toString();
+    label->setText(str);
+}
+
+void MainWindow::pushButtonClicked()
+{
+    /* 设置当前选定的日期为系统的QDate */
+    calendarWidget->setSelectedDate(QDate::currentDate());
+}
+```
+
+#### 运行效果说明
+
+点击日历改变日期,标签实时更新;点击“回到当前日期”,日历跳回系统当天并更新标签。
+
+---
+
+### 3.3 QLCDNumber
+
+#### 控件简介
+
+`QLCDNumber` 继承 `QFrame`,显示类似 LCD 的数字,支持任意大小的数字以及十进制、十六进制、八进制、二进制。通过 `display()` 槽可方便地接入数据源,该槽被重载以接受五种参数类型。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setDigitCount(int)` | 显示位数 |
+| `setSegmentStyle(QLCDNumber::SegmentStyle)` | 段样式(`Flat`、`Outline`、`Filled`) |
+| `display(...)` | 显示数字或字符串(重载槽) |
+| `setMode(QLCDNumber::Mode)` | 进制(`Dec`/`Hex`/`Oct`/`Bin`) |
+| `overflow()` | 溢出信号 |
+
+#### 用法示例
+
+**例 24_qlcdnumber LCDNumber 时钟**:定时器每秒刷新系统时间。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QLCDNumber>
+#include <QTimer>
+#include <QTime>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明QLCDNumber对象 */
+    QLCDNumber *lcdNumber;
+
+    /* 声明QTimer对象 */
+    QTimer *timer;
+
+private slots:
+    /* 槽函数 */
+    void timerTimeOut();
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体的大小与位置 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化与设置显示的位置大小*/
+    lcdNumber = new QLCDNumber(this);
+    lcdNumber->setGeometry(300, 200, 200, 50);
+
+    /* 设置显示的位数8位 */
+    lcdNumber->setDigitCount(8);
+    /* 设置样式 */
+    lcdNumber->setSegmentStyle(QLCDNumber::Flat);
+
+    /* 设置lcd显示为当前系统时间 */
+    QTime time = QTime::currentTime();
+
+    /* 设置显示的样式 */
+    lcdNumber->display(time.toString("hh:mm:ss"));
+
+    timer = new QTimer(this);
+    /* 设置定时器1000毫秒发送一个timeout()信号 */
+    timer->start(1000);
+
+    /* 信号槽连接 */
+    connect(timer, SIGNAL(timeout()), this,
+            SLOT(timerTimeOut()));
+
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::timerTimeOut()
+{
+    /* 当定时器计时1000毫秒后,刷新lcd显示当前系统时间 */
+    QTime time = QTime::currentTime();
+    /* 设置显示的样式 */
+    lcdNumber->display(time.toString("hh:mm:ss"));
+}
+```
+
+#### 运行效果说明
+
+程序启动后 LCD 显示当前系统时间,并每秒刷新,像数字时钟一样运行。
+
+---
+
+### 3.4 QProgressBar
+
+#### 控件简介
+
+`QProgressBar` 继承 `QWidget`,提供水平或垂直进度条,用于显示操作进度并确认程序仍在运行。常用于复制进度、加载进度等。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setRange(int min, int max)` | 范围 |
+| `setValue(int)` / `value()` | 当前值 |
+| `setFormat(QString)` | 文本格式,`%p%` 表示百分比 |
+| `setOrientation(Qt::Orientation)` | 方向 |
+| `setTextVisible(bool)` | 是否显示文本 |
+| `valueChanged(int)` | 值变化信号 |
+
+#### 用法示例
+
+**例 25_qprogressbar 手机电池充电**:定时器驱动进度条循环充电。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QProgressBar>
+#include <QTimer>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QProgressBar *progressBar;
+    QTimer *timer;
+
+    /* 用于设置当前QProgressBar的值 */
+    int value;
+
+private slots:
+    /* 槽函数 */
+    void timerTimeOut();
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    progressBar = new QProgressBar(this);
+    progressBar->setGeometry(300, 200, 200, 60);
+
+    /* 样式表设置,常用setStyleSheet来设置样式(实现界面美化的功能) */
+    progressBar->setStyleSheet (
+                "QProgressBar{border:8px solid #FFFFFF;"
+                "height:30;"
+                "border-image:url(:/images/battery.png);" //背景图片
+                "text-align:center;"    // 文字居中
+                "color:rgb(255,0,255);"
+                "font:20px;"      // 字体大小为20px
+                "border-radius:10px;}"
+                "QProgressBar::chunk{"
+                "border-radius:5px;" // 斑马线圆角
+                "border:1px solid black;" // 黑边,默认无边
+                "background-color:skyblue;"
+                "width:10px;margin:1px;}" // 宽度和间距
+                );
+
+    /* 设置progressBar的范围值 */
+    progressBar->setRange(0, 100);
+    /* 初始化value为0 */
+    value = 0;
+    /* 给progressBar设置当前值 */
+    progressBar->setValue(value);
+    /* 设置当前文本字符串的显示格式 */
+    progressBar->setFormat("充电中%p%");
+
+    /* 定时器实例化设置每100ms发送一个timeout信号 */
+    timer = new QTimer(this);
+    timer->start(100);
+
+    /* 信号槽连接 */
+    connect(timer, SIGNAL(timeout()),
+            this, SLOT(timerTimeOut()));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::timerTimeOut()
+{
+    /* 定时器时间到,value值自加一 */
+    value ++;
+    progressBar->setValue(value);
+    /* 若value值大于100,令value再回到0 */
+    if(value>100)
+        value = 0;
+}
+```
+
+#### 运行效果说明
+
+进度条以电池图片为底,每 100ms 前进一格,从 0% 充到 100% 后回到 0% 重新开始。
+
+---
+
+### 3.5 QFrame
+
+#### 控件简介
+
+`QFrame` 继承 `QWidget`,是所有带框架窗口部件的基类。它绘制框架并调用虚函数 `drawContents()` 填充框架,子类可重写该函数。`QPopupMenu` 用它把菜单“升高”,`QProgressBar` 有“凹陷”外观,`QLabel` 有平坦外观。
+
+`QFrame::Shape` 定义框架外形:
+
+| 取值 | 效果 |
+| ---- | ---- |
+| `NoFrame` | 不画任何东西 |
+| `Box` | 在内容周围画一个框 |
+| `Panel` | 画一个平板,内容看起来凸起或凹陷 |
+| `WinPanel` | 类似 Panel,但三维效果与 Windows 95 一致 |
+| `ToolBarPanel` | 调用 `QStyle::drawToolBarPanel()` |
+| `MenuBarPanel` | 调用 `QStyle::drawMenuBarPanel()` |
+| `HLine` | 画水平线,用于分隔 |
+| `VLine` | 画竖直线,用于分隔 |
+| `StyledPanel` | 调用 `QStyle::drawPanel()` |
+| `PopupPanel` | 调用 `QStyle::drawPopupPanel()` |
+
+阴影风格(`QFrame::Shadow`):`Plain`(无三维效果)、`Raised`(凸起)、`Sunken`(凹陷)。
+
+常用的还有 `drawFrame()` 与 `frameChanged()` 两个函数。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setFrameShape(QFrame::Shape)` | 框架外形 |
+| `setFrameShadow(QFrame::Shadow)` | 阴影风格 |
+| `frameRect()` | 框架矩形 |
+| `setFrameStyle(int)` | 同时设置外形与阴影 |
+
+#### 用法示例
+
+**例 26_qframe 水平/垂直线**:两条 `QFrame` 分别画水平线和垂直线。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QFrame>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QFrame *hline;
+    QFrame *vline;
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体的显示位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化 */
+    hline = new QFrame(this);
+    /* 确定起始点,设置长和宽,绘制矩形 */
+    hline->setGeometry(QRect(200, 100, 400, 40));
+
+    /* 设置框架样式为HLine,水平。由于是HLine,只显示一条水平直线 */
+    hline->setFrameShape(QFrame::HLine);
+    /* 绘制阴影 */
+    hline->setFrameShadow(QFrame::Sunken);
+
+    /* 实例化 */
+    vline = new QFrame(this);
+    /* 确定起始点,设置长和宽,绘制矩形 */
+    vline->setGeometry(QRect(300, 100, 2, 200));
+
+    /* 设置框架样式为VLine,垂直。由于是VLine,只显示一条垂直直线 */
+    vline->setFrameShape(QFrame::VLine);
+    /* 绘制阴影 */
+    vline->setFrameShadow(QFrame::Sunken);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+> Display Widgets 里的 Horizontal Line / Vertical Line 就是 `QFrame` 封装好的控件,也可以用本例的方式自行实现。
+
+#### 运行效果说明
+
+窗口中央出现一条水平直线和一条垂直直线。`QFrame` 常用于绘制边框美化界面,`QLabel` 等控件也能设置独特的边框样式。
+
+---
+
+## 4. 显示窗口部件之浏览器
+
+本节包含两个“浏览器”类控件:`QTextBrowser`(文本)与 `QGraphicsView`(图形)。两者都需要在项目里勾选 `mainwindow.ui`,以便使用自带的菜单栏、工具栏与状态栏。
+
+### 4.1 QTextBrowser
+
+#### 控件简介
+
+`QTextBrowser` 继承 `QTextEdit`,在只读 QTextEdit 的基础上增加了超文本导航功能,用户可以跟踪超文本文档中的链接。适合做一个能打开 txt、html 等文件的文本浏览器。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setPlainText(QString)` | 按纯文本设置内容 |
+| `setHtml(QString)` | 按 HTML 设置内容 |
+| `setSource(QUrl)` | 加载指定 URL |
+| `backward()` / `forward()` / `home()` | 后退/前进/主页 |
+| `anchorClicked(QUrl)` | 链接被点击信号 |
+| `setOpenExternalLinks(bool)` | 是否用外部浏览器打开链接 |
+
+#### 用法示例
+
+**例 27_qtextbrowser 简单的文本浏览器**:通过菜单栏“打开”动作加载文件,按后缀选择 HTML 或纯文本显示。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QTextBrowser>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }
+QT_END_NAMESPACE
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    Ui::MainWindow *ui;
+    /* 声明对象 */
+    QTextBrowser *textBrowser;
+    QAction *openAction;
+
+private slots:
+    /* 槽函数 */
+    void openActionTriggered();
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+/* 窗口对话框与文本流 */
+#include <QFileDialog>
+#include <QTextStream>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+    , ui(new Ui::MainWindow)
+{
+    ui->setupUi(this);
+    /* 设置主窗体位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 将窗口标题设置为文本浏览器 */
+    this->setWindowTitle("文本浏览器");
+
+    /* 实例化 */
+    textBrowser = new QTextBrowser(this);
+    /* 将文本浏览器窗口居中 */
+    this->setCentralWidget(textBrowser);
+
+    /* 实例化打开动作 */
+    openAction = new QAction("打开",this);
+    /* ui窗口自带有menubar(菜单栏)、mainToolbar(工具栏)与statusbar(状态栏)
+     * menuBar是ui生成工程就有的,可以在menubar里添加我们的QAction等 */
+    /* 将动作添加到菜单栏 */
+    ui->menubar->addAction(openAction);
+
+    /* 信号槽连接 */
+    connect(openAction, SIGNAL(triggered()),
+            this, SLOT(openActionTriggered()));
+
+}
+
+MainWindow::~MainWindow()
+{
+    delete ui;
+}
+
+void MainWindow::openActionTriggered()
+{
+    /* 调用系统打开文件窗口,过滤文件名 */
+    QString fileName = QFileDialog::getOpenFileName(
+                this,tr("打开文件"),"",
+                tr("Files(*.txt *.cpp *.h *.html *.htm)")
+                );
+    QFile myFile(fileName);
+    /* 以只读、文本方式打开,若打开失败,则返回 */
+    if(!myFile.open(QIODevice::ReadOnly | QIODevice::Text))
+        return;
+
+    /* 用QTextStream对象接收 */
+    QTextStream in (&myFile);
+
+    /* 读取全部数据 */
+    QString myText = in.readAll();
+
+    /* 判断打开文件的后缀,如果是html格式的则设置文本浏览器为html格式 */
+    if(fileName.endsWith("html") || fileName.endsWith("htm")){
+        textBrowser->setHtml(myText);
+    } else {
+        textBrowser->setPlainText(myText);
+    }
+
+    /* ui窗口自带有statusbar(状态栏),设置打开的文件名 */
+    ui->statusbar->showMessage("文件名:" + fileName);
+}
+```
+
+#### 运行效果说明
+
+菜单栏点击“打开”,在系统文件对话框中选择 `mainwindow.cpp`,内容显示在文本浏览器中,状态栏显示文件名。可自行扩展关闭、字体颜色、缩放等功能。(Ubuntu16 的 Qt 可能看不到菜单栏,Ubuntu18 正常。)
+
+---
+
+### 4.2 QGraphicsView
+
+#### 控件简介
+
+`QGraphicsView` 继承 `QAbstractScrollArea`,是图形视图框架的一部分,提供基于图元的模型/视图编程。它在可滚动视图中可视化 `QGraphicsScene` 的内容。
+
+要可视化场景,先构造一个 `QGraphicsView` 对象,把要可视化的场景地址传给它的构造函数;也可以稍后调用 `setScene()` 设置。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setScene(QGraphicsScene*)` | 设置场景 |
+| `scene()` | 取得场景 |
+| `QGraphicsScene::addPixmap(QPixmap)` | 向场景添加图元 |
+| `QGraphicsScene::clear()` | 清空场景 |
+| `QPixmap::scaled(w, h, aspectMode, transformMode)` | 缩放图像 |
+| `setRenderHint(...)` | 渲染质量提示 |
+
+#### 用法示例
+
+**例 28_qgraphicsview 简单的图像浏览器**:通过菜单打开图片,缩放后加入场景显示。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QGraphicsView>
+
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }
+QT_END_NAMESPACE
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    Ui::MainWindow *ui;
+    /* 声明对象 */
+    QGraphicsView *graphicsView;
+    QGraphicsScene *graphicsScene;
+    QAction *openAction;
+
+private slots:
+    /* 槽函数 */
+    void openActionTriggered();
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+#include <QFileDialog>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+    , ui(new Ui::MainWindow)
+{
+    ui->setupUi(this);
+    /* 设置主窗体大小 */
+    this->setGeometry(0, 0, 800, 480);
+    /* 将窗口标题设置为图像浏览器 */
+    this->setWindowTitle("图像浏览器");
+
+    /* 实例化图形视图对象 */
+    graphicsView = new QGraphicsView(this);
+    /* 将图像浏览器窗口居中 */
+    this->setCentralWidget(graphicsView);
+
+    /* 实例化场景对象 */
+    graphicsScene = new QGraphicsScene(this);
+
+    /* 在QGraphicsView设置场景 */
+    graphicsView->setScene(graphicsScene);
+
+    /* 将动作添加到菜单栏 */
+    openAction = new QAction("打开",this);
+    ui->menubar->addAction(openAction);
+
+    /* 信号槽连接 */
+    connect(openAction, SIGNAL(triggered()),
+            this, SLOT(openActionTriggered()));
+}
+
+MainWindow::~MainWindow()
+{
+    delete ui;
+}
+
+void MainWindow::openActionTriggered()
+{
+    /* 调用系统打开文件窗口,设置窗口标题为“打开文件”,过滤文件名 */
+    QString fileName = QFileDialog::getOpenFileName(
+                this,tr("打开文件"), "",
+                tr("Files(*.png *.jpg *.bmp)")
+                );
+    /* 定义QPixmap对象,指向fileName */
+    QPixmap image(fileName);
+
+    /* 假若用户没选择文件,则返回 */
+    if(image.isNull())
+        return;
+    /* 把image缩放为graphicsView的长宽,保持纵横比,使用快速变换 */
+    image = image.scaled(graphicsView->width(),
+                          graphicsView->height(),
+                          Qt::KeepAspectRatio,
+                          Qt::FastTransformation
+                          );
+    /* 在添加场景内容前,先清除之前的场景内容 */
+    graphicsScene->clear();
+    /* 添加场景内容image */
+    graphicsScene->addPixmap(image);
+    /* ui窗口自带有statusBar(状态栏),设置打开的文件名 */
+    ui->statusbar->showMessage("文件名:" + fileName);
+}
+```
+
+#### 运行效果说明
+
+菜单栏点击“打开”,选择一张图片,图片按视口大小等比缩放后显示在窗口中,状态栏显示文件名。
+
+---
+
+## 5. 总结
+
+### 5.1 按用途记忆
+
+```mermaid
+mindmap
+  root((Qt控件<br/>7.1-7.4))
+    按钮
+      QPushButton 命令
+      QToolButton 工具栏图标
+      QRadioButton 多选一
+      QCheckBox 多选多/三态
+      QCommandLinkButton 链接
+      QDialogButtonBox 对话框按钮
+    输入
+      QComboBox 下拉
+      QFontComboBox 字体
+      QLineEdit 单行
+      QTextEdit 富文本
+      QPlainTextEdit 纯文本
+      QSpinBox 整数
+      QDoubleSpinBox 浮点
+      QDateTimeEdit 日期时间
+      QDial 拨盘
+      QScrollBar 滚动条
+      QSlider 滑动条
+      QKeySequenceEdit 快捷键
+    显示
+      QLabel 文本/图像
+      QCalendarWidget 日历
+      QLCDNumber 数字
+      QProgressBar 进度
+      QFrame 边框/分隔线
+    浏览器
+      QTextBrowser 超文本
+      QGraphicsView 图形场景
+```
+
+### 5.2 关键规律
+
+| 规律 | 说明 |
+| ---- | ---- |
+| 位置与大小 | 所有 `QWidget` 子类都可用 `setGeometry(x, y, w, h)` |
+| 信号槽写法 | 例程统一使用 `connect(obj, SIGNAL(...), this, SLOT(...))` |
+| 容器 | `QWidget` 可作容器;`setCentralWidget()` 把控件设为主窗口中央部件 |
+| 布局 | 例程大量使用 `setGeometry` 绝对定位,下一篇将学习布局管理器 |
+| 样式表 | `setStyleSheet()` 可实现换肤、充电条、开关等美化效果 |
+| 资源路径 | 引用 qrc 资源格式为 `:/前缀/文件路径`,如 `:/images/battery.png` |
+
+---
+
+## 6. 构建运行步骤
+
+1. **新建项目**:Qt Creator → `File` → `New File or Project` → `Application (Qt)` → `Qt Widgets Application`;项目名如 `04_qpushbutton`;基类选 `QMainWindow`;**不勾选** “Generate form”(例程用纯代码,不用 `.ui`)。
+2. **替换源码**:把 `mainwindow.h`、`mainwindow.cpp` 替换为本节代码;`main.cpp` 若无特别说明保持新建时的默认内容。
+3. **添加资源**(涉及 qss/图片的例程):参照 1.3 节步骤添加 `.qrc`、图片,并创建 `style.qss`。
+4. **检查 `.pro` 文件**:Qt Widgets 项目默认包含:
+
+   ```qmake
+   QT       += core gui
+   greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+   CONFIG += c++11
+   SOURCES += main.cpp mainwindow.cpp
+   HEADERS += mainwindow.h
+   ```
+
+5. **编译运行**:`Ctrl+R`(构建并运行)或 `Ctrl+B`(仅构建)。
+6. **交叉编译到开发板**:参照《I.MX6U嵌入式Qt开发指南》第十四章配置交叉编译环境,将可执行文件拷贝到板子后运行(需带 Qt 库与字体)。
+
+---
+
+## 7. 面试题
+
+### Q1:`QPushButton`、`QRadioButton`、`QCheckBox` 的共同基类是什么?它们的选择模式有何区别?
+
+共同基类是 `QAbstractButton`。`QPushButton` 用于执行命令/触发事件,一般不可切换;`QRadioButton` 单选,同组内“多选一”;`QCheckBox` 复选,允许多选多,并可通过 `setTristate()` 开启半选状态。
+
+### Q2:`QTextEdit` 和 `QPlainTextEdit` 有什么区别?什么时候用哪个?
+
+`QTextEdit` 支持富文本(HTML 样式、图片、列表、表格),功能强;`QPlainTextEdit` 只支持纯文本,可理解为前者的低配版。处理大量文字、频繁滚动时,`QPlainTextEdit` 的显示效率更高,应优先使用。
+
+### Q3:`QComboBox` 与 `QFontComboBox` 的关系是什么?如何取得当前选项的文本?
+
+`QFontComboBox` 继承 `QComboBox`,是一个专门填充系统字体家族的下拉框,其字体变化信号为 `currentFontChanged(QFont)`。`QComboBox` 中可用 `itemText(currentIndex())` 取得当前选项文本,或直接用 `currentText()`。
+
+### Q4:`QSpinBox` 与 `QDoubleSpinBox` 都可以用 `valueChanged` 信号,为什么它们的槽函数参数类型不同?
+
+因为两者处理的数据类型不同:`QSpinBox` 处理整数,信号是 `valueChanged(int)`;`QDoubleSpinBox` 处理浮点值,信号是 `valueChanged(double)`。连接时槽函数参数类型必须与信号一致。
+
+### Q5:为什么用 `QTextEdit::selectAll()` 全选前要先 `setFocus()`?`QTextBrowser` 与 `QTextEdit` 又是什么关系?
+
+`selectAll()` 作用于当前有焦点的编辑区域,如果控件没有焦点,选中可能不生效,因此例程先调用 `setFocus()`。`QTextBrowser` 继承 `QTextEdit`,在只读模式下扩展了超文本导航(链接跳转、前进/后退),用于显示带链接的文档。
+
+---
+
+**内容来源**:《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第七章 Qt控件 7.1 按钮、7.2 输入窗口部件、7.3 显示窗口部件、7.4 显示窗口部件之浏览器;配套例程 `Embedded-Qt-Tutorial/Qt/01/04_qpushbutton` ~ `28_qgraphicsview`。

+ 2557 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/05-布局容器与模型视图.md

@@ -0,0 +1,2557 @@
+---
+title: 布局容器与模型视图
+tags: [嵌入式Linux, Qt, Qt Widgets, 布局管理, QBoxLayout, QGridLayout, QFormLayout, QSpacerItem, QGroupBox, QScrollArea, QToolBox, QTabWidget, QStackedWidget, QWidget, QMdiArea, QDockWidget, QListView, QTreeView, QTableView, QColumnView, QUndoView, QListWidget, QTreeWidget, QTableWidget, 模型视图, I.MX6U]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第七章 Qt控件 7.5 布局管理、7.6 空间间隔、7.7 容器、7.8 项目视图组(基于模型)、7.9 项目控件组(基于项);例程 Embedded-Qt-Tutorial/Qt/01/29_qboxlayout~47_qtablewidget"
+---
+
+# 布局容器与模型视图
+
+> 💡 **关联知识**:[[05-Qt应用开发/03-信号与槽]]、[[05-Qt应用开发/04-Qt控件详解]];延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/7. 布局管理器]]
+
+本篇覆盖《I.MX6U嵌入式Qt开发指南》第七章的后半部分:**布局管理**(7.5)、**空间间隔**(7.6)、**容器**(7.7)、**项目视图组(基于模型)**(7.8)、**项目控件组(基于项)**(7.9),共 20 个类。代码取自配套例程 `29_qboxlayout` ~ `47_qtablewidget`,去掉版权后可直接编译。
+
+上一篇使用了 `setGeometry()` 绝对定位,界面一旦缩放就会错位。本篇的布局管理器正好解决这个问题;而模型/视图部分则解决“大量结构化数据如何高效显示”的问题。
+
+---
+
+## 0. 全章总览
+
+### 0.1 布局类继承关系
+
+Qt 的基本布局管理类都继承自 `QLayout`,而 `QLayout` 继承自 `QObject`(不是 `QWidget`)。多个布局可以彼此嵌套,构建复杂界面。
+
+```mermaid
+classDiagram
+    class QObject
+    class QLayout
+    class QBoxLayout
+    class QHBoxLayout
+    class QVBoxLayout
+    class QGridLayout
+    class QFormLayout
+    class QStackedLayout
+    class QLayoutItem
+    class QSpacerItem
+    class QWidgetItem
+
+    QObject <|-- QLayout
+    QLayout <|-- QBoxLayout
+    QBoxLayout <|-- QHBoxLayout
+    QBoxLayout <|-- QVBoxLayout
+    QLayout <|-- QGridLayout
+    QLayout <|-- QFormLayout
+    QLayout <|-- QStackedLayout
+    QObject <|-- QLayoutItem
+    QLayoutItem <|-- QSpacerItem
+    QLayoutItem <|-- QWidgetItem
+```
+
+各布局作用:
+
+| 布局 | 作用 |
+| ---- | ---- |
+| `QBoxLayout` | 水平或垂直排列子部件 |
+| `QGridLayout` | 网格(二维)方式管理界面组件 |
+| `QFormLayout` | 以两列形式布局输入部件与其标签 |
+| `QStackedLayout` | 一组布局后的部件,可分布显示 |
+
+### 0.2 容器与模型/视图索引
+
+| 分组 | 控件 | 一句话作用 | 例程 |
+| ---- | ---- | ---------- | ---- |
+| 布局 | `QBoxLayout`(`QHBoxLayout`/`QVBoxLayout`) | 水平/垂直布局 | 29 |
+| 布局 | `QGridLayout` | 网格布局 | 30 |
+| 布局 | `QFormLayout` | 表单布局 | 31 |
+| 间隔 | `QSpacerItem` | 布局中的空白间隔 | 32 |
+| 容器 | `QGroupBox` | 带标题的组框 | 33 |
+| 容器 | `QScrollArea` | 滚动区域 | 34 |
+| 容器 | `QToolBox` | 列状层叠工具箱(抽屉) | 35 |
+| 容器 | `QTabWidget` | 选项卡多页面 | 36 |
+| 容器 | `QStackedWidget` | 堆叠小部件,一次显示一个 | 37 |
+| 容器 | `QWidget` | 所有 UI 对象的基类,可作容器 | 无独立例程 |
+| 容器 | `QMdiArea` | 多文档界面(MDI)区域 | 38 |
+| 容器 | `QDockWidget` | 可停靠窗体部件 | 39 |
+| 模型视图 | `QListView` | 基于模型的列表/图标视图 | 40 |
+| 模型视图 | `QTreeView` | 基于模型的树视图 | 41 |
+| 模型视图 | `QTableView` | 基于模型的表视图 | 42 |
+| 模型视图 | `QColumnView` | 级联列表视图 | 43 |
+| 模型视图 | `QUndoView` | 撤销堆栈视图 | 44 |
+| 项目控件 | `QListWidget` | 基于项的列表 | 45 |
+| 项目控件 | `QTreeWidget` | 基于项的树 | 46 |
+| 项目控件 | `QTableWidget` | 基于项的表 | 47 |
+
+### 0.3 模型/视图与基于项的区别
+
+- `QListView` / `QTreeView` / `QTableView` / `QColumnView` 基于 **Model**,需要自己建模(如 `QStringListModel`、`QStandardItemModel`),数据与显示分离,减少数据冗余、程序效率高。
+- `QListWidget` / `QTreeWidget` / `QTableWidget` 基于 **Item**,是“已经封装好内部模型的视图”,直接 `addItem`、`setItem` 即可,操作方便,适合中小数据量。
+
+Qt 提供的常用现成模型:
+
+| 模型 | 用途 |
+| ---- | ---- |
+| `QStringListModel` | 存储简单的 `QString` 列表 |
+| `QStandardItemModel` | 管理复杂的树型结构数据项,每项可包含任意数据 |
+| `QDirModel` | 提供本地文件系统的文件与目录信息 |
+| `QSqlQueryModel` / `QSqlTableModel` / `QSqlRelationTableModel` | 访问数据库 |
+
+---
+
+## 1. 布局管理
+
+### 1.1 QBoxLayout
+
+#### 控件简介
+
+`QBoxLayout` 继承 `QLayout`,提供水平或垂直排列子部件。它从父布局或 `parentWidget()` 获取空间,分成一列框,并让每个被管理的部件填充一个框。
+
+`QHBoxLayout`(水平)与 `QVBoxLayout`(垂直)都继承自 `QBoxLayout`。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QHBoxLayout()` / `QVBoxLayout()` | 构造水平/垂直布局 |
+| `addWidget(QWidget*)` | 添加部件 |
+| `addLayout(QLayout*)` | 嵌套添加子布局 |
+| `addStretch(int)` | 添加伸缩量 |
+| `addSpacerItem(QSpacerItem*)` | 添加间隔 |
+| `setSpacing(int)` | 设置部件间距 |
+| `setMargin(int)` / `setContentsMargins(...)` | 设置外框留白 |
+| `setDirection(Direction)` | 设置排列方向 |
+| `setStretch(int, int)` | 设置某部件的伸缩因子 |
+
+#### 用法示例
+
+**例 29_qboxlayout 垂直或水平布局**:上半区水平排 3 个按钮,下半区垂直排 3 个按钮。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明按钮对象数组 */
+    QPushButton *pushButton[6];
+
+    /* 声明两个widget,用于容纳排布按钮 */
+    QWidget *hWidget;
+    QWidget *vWidget;
+
+    /* QHBoxLayout与QVBoxLayout对象 */
+    QHBoxLayout *hLayout;
+    QVBoxLayout *vLayout;
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include <QList>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗口的位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化与设置位置大小 */
+    hWidget = new QWidget(this);
+    hWidget->setGeometry(0, 0, 800, 240);
+
+    vWidget = new QWidget(this);
+    vWidget->setGeometry(0, 240, 800, 240);
+
+    hLayout = new QHBoxLayout();
+    vLayout = new QVBoxLayout();
+
+    /* QList<T>是Qt的一种泛型容器类。
+     * 它以链表方式存储一组值,并能对这组数据进行快速索引
+     */
+    QList <QString>list;
+    /* 将字符串值插入list */
+    list<<"one"<<"two"<<"three"<<"four"<<"five"<<"six";
+
+    /* 用一个循环实例化6个按钮 */
+    for(int i = 0; i < 6; i++){
+        pushButton[i] = new QPushButton();
+        pushButton[i]->setText(list[i]);
+        if(i < 3) {
+            /* 将按钮添加至hLayout中 */
+            hLayout->addWidget(pushButton[i]);
+        } else {
+            /* 将按钮添加至vLayout中 */
+            vLayout->addWidget(pushButton[i]);
+        }
+    }
+    /* 设置间隔为50 */
+    hLayout->setSpacing(50);
+
+    /* hWidget与vWidget的布局设置为hLayout/vLayout */
+    hWidget->setLayout(hLayout);
+    vWidget->setLayout(vLayout);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+上半区 3 个按钮水平排布,下半区 3 个按钮垂直排布,间距为 50 像素。
+
+---
+
+### 1.2 QGridLayout
+
+#### 控件简介
+
+`QGridLayout` 继承 `QLayout`,以网格(二维)方式管理界面组件,坐标与二维数组类似:
+
+```
+0,0(组件一)   0,1(组件二)
+1,0(组件三)   1,1(组件四)
+```
+
+它把可用空间分为行和列,把部件放入正确单元格。与 `QBoxLayout` 不同的是,网格布局还需要分别设置行和列的比例系数(拉伸因子)。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addWidget(QWidget*, int row, int col)` | 把部件放入指定单元格 |
+| `addWidget(QWidget*, row, col, rowSpan, colSpan)` | 跨行/跨列 |
+| `setRowStretch(int row, int stretch)` | 行比例系数 |
+| `setColumnStretch(int col, int stretch)` | 列比例系数 |
+| `setSpacing(int)` | 间距 |
+| `setMargin(int)` | 外框留白 |
+
+#### 用法示例
+
+**例 30_qgridlayout 网格布局**:4 个按钮排成 2×2,并设置行列拉伸因子。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QGridLayout>
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+private:
+
+    /* 声明widget窗口部件,用于容纳下面4个pushButton按钮 */
+    QWidget *gWidget;
+
+    /* 声明QGridLayout对象 */
+    QGridLayout *gridLayout;
+
+    /* 声明pushButton按钮数组 */
+    QPushButton *pushButton[4];
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化 */
+    gWidget = new QWidget(this);
+    /* 设置gWidget居中央 */
+    this->setCentralWidget(gWidget);
+
+    gridLayout = new QGridLayout();
+    /* QList链表,字符串类型 */
+    QList <QString> list;
+    list<<"按钮1"<<"按钮2"<<"按钮3"<<"按钮4";
+    for (int i = 0; i < 4; i++){
+        pushButton[i] = new QPushButton();
+        pushButton[i]->setText(list[i]);
+        /* 设置最小宽度与高度 */
+        pushButton[i]->setMinimumSize(100, 30);
+        /* 自动调整按钮的大小 */
+        pushButton[i]->setSizePolicy(
+                    QSizePolicy::Expanding,
+                    QSizePolicy::Expanding
+                    );
+        switch (i) {
+        case 0:
+            /* 将pushButton[0]添加至网格的坐标(0,0),下同 */
+            gridLayout->addWidget(pushButton[i], 0, 0);
+            break;
+        case 1:
+            gridLayout->addWidget(pushButton[i], 0, 1);
+            break;
+        case 2:
+            gridLayout->addWidget(pushButton[i], 1, 0);
+            break;
+        case 3:
+            gridLayout->addWidget(pushButton[i], 1, 1);
+            break;
+        default:
+            break;
+        }
+    }
+    /* 设置第0行与第1行的行比例系数 */
+    gridLayout->setRowStretch(0, 2);
+    gridLayout->setRowStretch(1, 3);
+
+    /* 设置第0列与第1列的列比例系数 */
+    gridLayout->setColumnStretch(0, 1);
+    gridLayout->setColumnStretch(1, 3);
+
+    /* 将gridLayout设置到gWidget */
+    gWidget->setLayout(gridLayout);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+4 个按钮按 2×2 网格排列;行高比例为 2:3,列宽比例为 1:3,因此按钮大小并不均等。
+
+---
+
+### 1.3 QFormLayout
+
+#### 控件简介
+
+`QFormLayout` 继承 `QLayout`,以两列形式管理输入部件及其标签:左列是标签,右列是“字段”小部件(如 `QLineEdit`、`QSpinBox`)。适合行列较少的表单;多行多列应改用 `QGridLayout`。可用 `setRowWrapPolicy(RowWrapPolicy)` 设置换行策略。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addRow(const QString &labelText, QWidget *field)` | 添加一行:标签文本 + 字段部件 |
+| `addRow(QWidget *label, QWidget *field)` | 用部件作标签 |
+| `setSpacing(int)` | 水平/垂直间距 |
+| `setMargin(int)` | 布局外框宽度 |
+| `setRowWrapPolicy(RowWrapPolicy)` | 换行策略 |
+| `setLabelAlignment(Qt::Alignment)` | 标签对齐 |
+| `setFormAlignment(Qt::Alignment)` | 表单对齐 |
+
+#### 用法示例
+
+**例 31_qformlayout 表单布局**:用户名、密码两行输入。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QFormLayout>
+#include <QLineEdit>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+private:
+    /* widget对象 */
+    QWidget *fWidget;
+
+    /* 用于输入用户名 */
+    QLineEdit *userLineEdit;
+
+    /* 用于输入密码 */
+    QLineEdit *passwordLineEdit;
+
+    /* 声明QFormLayout对象 */
+    QFormLayout *formLayout;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化及设置位置与大小,下同 */
+    fWidget = new QWidget(this);
+    fWidget->setGeometry(250, 100, 300, 200);
+
+    userLineEdit = new QLineEdit();
+    passwordLineEdit = new QLineEdit();
+
+    formLayout = new QFormLayout();
+
+    /* 添加行 */
+    formLayout->addRow("用户名:", userLineEdit);
+    formLayout->addRow("密码    :", passwordLineEdit);
+
+    /* 设置水平垂直间距 */
+    formLayout->setSpacing(10);
+
+    /* 设置布局外框的宽度 */
+    formLayout->setMargin(20);
+
+    /* 将formLayout布局到fWidget */
+    fWidget->setLayout(formLayout);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+`fWidget` 内出现两行:左侧“用户名:”“密码 :”,右侧对应的单行输入框,行列间距与外框留白已设置。
+
+---
+
+## 2. 空间间隔
+
+### 2.1 QSpacerItem
+
+#### 控件简介
+
+`QSpacerItem` 继承 `QLayoutItem`,在布局中提供空白(空间间隔),只能在布局里使用。它对应 Qt Designer 中的 Horizontal Spacer(水平间隔)与 Vertical Spacer(垂直间隔)。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QSpacerItem(int w, int h, QSizePolicy::Policy hPolicy, QSizePolicy::Policy vPolicy)` | 构造间隔 |
+| `changeSize(int w, int h, ...)` | 改变尺寸策略 |
+| `QLayout::addSpacerItem(QSpacerItem*)` | 把间隔加入布局 |
+| `QLayout::addStretch(int)` | 直接加伸缩量(更简便) |
+
+#### 用法示例
+
+**例 32_qspaceritem 空间间隔**:垂直布局中加垂直间隔,水平布局中加水平间隔。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QPushButton>
+#include <QSpacerItem>
+#include <QBoxLayout>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 按钮对象数组 */
+    QPushButton *bt[4];
+    /* 垂直间隔 */
+    QSpacerItem *vSpacer;
+    /* 水平间隔 */
+    QSpacerItem *hSpacer;
+    /* 声明一个widget用来存放布局的内容 */
+    QWidget *widget;
+    /* 主布局对象 */
+    QHBoxLayout *mainLayout;
+    /* 垂直布局对象 */
+    QVBoxLayout *vBoxLayout;
+    /* 水平布局对象 */
+    QHBoxLayout *hBoxLayout;
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体显示位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    widget = new QWidget(this);
+    /* 居中widget */
+    this->setCentralWidget(widget);
+
+    /* 实例化对象 */
+    vSpacer  =  new QSpacerItem(10, 10,
+                                QSizePolicy::Minimum,
+                                QSizePolicy::Expanding
+                                );
+    hSpacer  =  new QSpacerItem(10, 10,
+                                QSizePolicy::Expanding,
+                                QSizePolicy::Minimum
+                                );
+
+    vBoxLayout = new QVBoxLayout();
+    hBoxLayout = new QHBoxLayout();
+    mainLayout = new QHBoxLayout();
+
+    /* 在vBoxLayout添加垂直间隔 */
+    vBoxLayout->addSpacerItem(vSpacer);
+
+    QList <QString>list;
+    /* 将字符串值插入list */
+    list<<"按钮1"<<"按钮2"<<"按钮3"<<"按钮4";
+    /* 用一个循环实例化4个按钮 */
+    for(int i = 0; i < 4 ; i++){
+        bt[i] = new QPushButton();
+        bt[i]->setText(list[i]);
+        if (i == 0){
+            /* 按钮1,设置为100*100 */
+            bt[i]->setFixedSize(100, 100);
+            /* 在vBoxLayout添加按钮1 */
+            vBoxLayout->addWidget(bt[i]);
+        } else {
+            /* 按钮2~4,设置为60*60 */
+            bt[i]->setFixedSize(60, 60);
+            /* 在hBoxLayout添加按钮2~4 */
+            hBoxLayout->addWidget(bt[i]);
+        }
+    }
+    /* 在hBoxLayout添加水平间隔 */
+    hBoxLayout->addSpacerItem(hSpacer);
+
+    /* 在主布局里添加垂直布局 */
+    mainLayout->addLayout(vBoxLayout);
+    /* 在主布局里添加水平布局 */
+    mainLayout->addLayout(hBoxLayout);
+
+    /* 设置部件间距 */
+    mainLayout->setSpacing(50);
+    /* 将主布局设置为widget的布局 */
+    widget->setLayout(mainLayout);
+
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+垂直布局中,垂直间隔把按钮 1 顶到下方;水平布局中,按钮 2~4 之后是水平间隔,把按钮挤向左侧。
+
+---
+
+## 3. 容器
+
+容器类用于分组、容纳其他窗口部件。下面依次讲解 `QGroupBox`、`QScrollArea`、`QToolBox`、`QTabWidget`、`QStackedWidget`、`QWidget`、`QMdiArea`、`QDockWidget`。
+
+### 3.1 QGroupBox
+
+#### 控件简介
+
+`QGroupBox` 继承 `QWidget`,提供一个带标题的组框框架,通常与一组同类型部件一起使用。组框还提供键盘快捷方式,可把焦点移到组框内的一个子部件。内部一般使用布局(如 `QBoxLayout`)来布置子部件。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QGroupBox(const QString &title, QWidget *parent)` | 构造:标题 + 父对象 |
+| `setTitle(QString)` / `title()` | 标题 |
+| `setCheckable(bool)` | 是否带复选框 |
+| `setChecked(bool)` | 复选状态 |
+| `setLayout(QLayout*)` | 设置内部布局 |
+| `toggled(bool)` | 复选状态变化信号 |
+
+#### 用法示例
+
+**例 33_qgroupbox 组框示例**:3 个单选框放进一个组框。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QRadioButton>
+#include <QGroupBox>
+#include <QVBoxLayout>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QGroupBox *groupBox;
+    QVBoxLayout  *vBoxLayout;
+    QRadioButton *radioButton[3];
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include <QList>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+    /* 以标题为“QGroupBox示例”实例化groupBox对象 */
+    groupBox = new QGroupBox(tr("QGroupBox示例"), this);
+    groupBox->setGeometry(300, 100, 300, 200);
+
+    vBoxLayout = new QVBoxLayout();
+
+    /* 字符串链表 */
+    QList <QString>list;
+    list<<"选项一"<<"选项二"<<"选项三";
+    for(int i = 0; i < 3; i++){
+        radioButton[i] = new QRadioButton();
+        radioButton[i]->setText(list[i]);
+        /* 在vBoxLayout添加radioButton */
+        vBoxLayout->addWidget(radioButton[i]);
+    }
+    /* 添加一个伸缩量1 */
+    vBoxLayout->addStretch(1);
+    /* vBoxLayout布局设置为groupBox布局 */
+    groupBox->setLayout(vBoxLayout);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+“QGroupBox 示例”组框内规则地垂直排列三个单选框。
+
+---
+
+### 3.2 QScrollArea
+
+#### 控件简介
+
+`QScrollArea` 继承 `QAbstractScrollArea`,用于在框架中显示子部件的内容。当子部件超过框架大小时,视图出现滚动条,从而可以查看子部件的整个区域。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setWidget(QWidget*)` | 设置要显示的子部件 |
+| `widget()` | 取得子部件 |
+| `setWidgetResizable(bool)` | 是否随区域自动调整大小 |
+| `setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy)` | 水平滚动条策略 |
+| `setVerticalScrollBarPolicy(Qt::ScrollBarPolicy)` | 垂直滚动条策略 |
+
+#### 用法示例
+
+**例 34_qscrollarea 滚动视图**:把一张大图放进滚动区域。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QScrollArea>
+#include <QLabel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 定义QScrollArea对象 */
+    QScrollArea *scrollArea;
+    QLabel *label;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    this->setGeometry(0, 0, 800, 480);
+
+    scrollArea = new QScrollArea(this);
+    /* 设置滚动区域为700*380 */
+    scrollArea->setGeometry(50, 50, 700, 380);
+
+    label = new QLabel();
+    /* label显示的lantingxu.png图片分辨率为1076*500 */
+    QImage image(":/images/lantingxu.png");
+    label->setPixmap(QPixmap::fromImage(image));
+
+    scrollArea->setWidget(label);
+
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+图片(1076×500)大于滚动区域(700×380),因此出现滚动条,可拖动查看图片其余部分。
+
+---
+
+### 3.3 QToolBox
+
+#### 控件简介
+
+`QToolBox` 继承 `QFrame`,提供一列选项卡小部件项,中文常译为“工具箱”,效果类似抽屉或 QQ 好友面板。当前项显示在当前选项卡下面,每个选项卡在选项卡列中都有一个索引位置,选项卡项是 `QWidget`。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addItem(QWidget *widget, const QString &text)` | 添加页 |
+| `insertItem(int index, QWidget*, QString)` | 插入页 |
+| `removeItem(int index)` | 移除页 |
+| `count()` | 页数 |
+| `setCurrentIndex(int)` / `currentIndex()` | 当前页 |
+| `currentChanged(int)` | 当前页变化信号 |
+
+#### 用法示例
+
+**例 35_qtoolbox QQ 好友面板**:两组工具按钮分别放进两个组框,再作为两页加入工具箱。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QToolBox>
+#include <QGroupBox>
+#include <QToolButton>
+#include <QVBoxLayout>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QToolBox *toolBox;
+    QGroupBox *groupBox[2];
+    QVBoxLayout  *vBoxLayout[2];
+    QToolButton *toolButton[6];
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    this->setGeometry(0, 0, 800, 480);
+
+    toolBox = new QToolBox(this);
+    toolBox->setGeometry(300, 50, 200, 250);
+    /* 设置toolBox的样式,此处设置为30%不透明度的黑色 */
+    toolBox->setStyleSheet("QToolBox {background-color:rgba(0, 0, 0, 30%);}");
+
+    for(int i = 0; i < 2; i++){
+        vBoxLayout[i] = new QVBoxLayout();
+        groupBox[i] = new QGroupBox(this);
+    }
+
+    /* 字符串链表 */
+    QList <QString>strList;
+    strList<<"李白"<<"王照君"<<"李元芳"<<"程咬金"<<"钟馗"<<"上官婉儿";
+
+    /* 字符串图标链表 */
+    QList <QString>iconsList;
+    iconsList<<":/icons/libai"<<":/icons/wangzhaojun"
+            <<":/icons/liyuanfang"<<":/icons/chengyaojin"
+           <<":/icons/zhongkui"<<":/icons/shangguanwaner";
+
+    for(int i = 0; i < 6; i++){
+        toolButton[i] = new QToolButton();
+        /* 设置toolButton图标 */
+        toolButton[i]->setIcon(QIcon(iconsList[i]));
+        /* 设置toolButton的文本 */
+        toolButton[i]->setText(strList[i]);
+        /* 设置toolButton的大小 */
+        toolButton[i]->setFixedSize(150, 40);
+        /* 设置toolButton的setToolButtonStyle的样式 */
+        toolButton[i]->setToolButtonStyle(
+                    Qt::ToolButtonTextBesideIcon
+                    );
+        if( i < 3 ) {
+            /* 将toolButton添加到垂直布局 */
+            vBoxLayout[0]->addWidget(toolButton[i]);
+            /* 添加一个伸缩量1 */
+            vBoxLayout[0]->addStretch(1);
+        } else {
+            vBoxLayout[1]->addWidget(toolButton[i]);
+            vBoxLayout[1]->addStretch(1);
+        }
+    }
+    /* 将垂直布局的内容添加到组框groupBox */
+    groupBox[0]->setLayout(vBoxLayout[0]);
+    groupBox[1]->setLayout(vBoxLayout[1]);
+
+    /* 将组框加入QToolBox里 */
+    toolBox->addItem(groupBox[0],"我的好友");
+    toolBox->addItem(groupBox[1],"黑名单");
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+点击“我的好友”显示前三人的头像列表,点击“黑名单”显示后三人的列表。子项除 `QGroupBox` 外也可以是普通 `QWidget`。
+
+---
+
+### 3.4 QTabWidget
+
+#### 控件简介
+
+`QTabWidget` 继承 `QWidget`,提供一组选项卡(多页面)小部件,用于分页显示:每一页一个界面,众多界面公用一块区域,节省空间。类似浏览器的多标签页。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addTab(QWidget *page, const QString &label)` | 添加页面 |
+| `addTab(QWidget *page, QIcon, QString)` | 带图标添加 |
+| `insertTab(int, QWidget*, QIcon, QString)` | 插入页面 |
+| `removeTab(int index)` | 移除页面 |
+| `setCurrentIndex(int)` / `currentIndex()` | 当前页 |
+| `setTabsClosable(bool)` | 是否显示关闭按钮 |
+| `setTabPosition(QTabWidget::TabPosition)` | 页签位置 |
+| `currentChanged(int)` | 当前页变化信号 |
+| `tabCloseRequested(int)` | 页签关闭请求信号 |
+
+#### 用法示例
+
+**例 36_qtabwidget 标题栏多页面切换**:3 个页面各放一个标签。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QTabWidget>
+#include <QHBoxLayout>
+#include <QLabel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QWidget *widget;
+    QTabWidget *tabWidget;
+    QHBoxLayout  *hBoxLayout;
+    QLabel *label[3];
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    this->setGeometry(0, 0, 800, 480);
+
+    widget = new QWidget(this);
+    /* 居中 */
+    this->setCentralWidget(widget);
+
+    /* 多页面小部件 */
+    tabWidget = new QTabWidget();
+
+    /* 水平布局实例化 */
+    hBoxLayout = new QHBoxLayout();
+    QList <QString>strLabelList;
+    strLabelList<<"标签一"<<"标签二"<<"标签三";
+
+    QList <QString>strTabList;
+    strTabList<<"页面一"<<"页面二"<<"页面三";
+
+    QList <QString>iconList;
+    iconList<<":/icons/icon1"
+           <<":/icons/icon2"
+          <<":/icons/icon3";
+
+    for (int i = 0; i < 3; i++) {
+        label[i] = new QLabel();
+        /* 设置标签文本 */
+        label[i]->setText(strLabelList[i]);
+        /* 标签对齐方式(居中) */
+        label[i]->setAlignment(Qt::AlignCenter);
+        /* 添加页面 */
+        tabWidget->addTab(label[i],
+                          QIcon(iconList[i]),
+                          strTabList[i]
+                          );
+    }
+    /* 是否添加关闭按钮 */
+    //tabWidget->setTabsClosable(true);
+    /* 将tabWidget水平排布 */
+    hBoxLayout->addWidget(tabWidget);
+    /* 将布局设置到widget */
+    widget->setLayout(hBoxLayout);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+> 可扩展使用 `setTabsClosable(true)` 在页签后加关闭按钮,再连接 `tabCloseRequested(int)` 与 `removeTab(int)` 实现关闭页面。
+
+#### 运行效果说明
+
+点击不同页签切换到对应页面,页面中央显示“标签一/二/三”。页签带图标。
+
+---
+
+### 3.5 QStackedWidget
+
+#### 控件简介
+
+`QStackedWidget` 继承 `QFrame`,提供一个小部件堆栈,一次只能看到一个小部件,类似 QQ 设置面板。它可用于创建类似 `QTabWidget` 的界面,构建在 `QStackedLayout` 之上。常与 `QListWidget` 搭配:左边列表选择,右边堆叠显示不同页面。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addWidget(QWidget*)` | 添加页面 |
+| `insertWidget(int, QWidget*)` | 插入页面 |
+| `removeWidget(QWidget*)` | 移除页面 |
+| `setCurrentIndex(int)` / `currentIndex()` | 当前页(对应槽 `setCurrentIndex(int)`) |
+| `count()` | 页面数 |
+| `currentChanged(int)` | 当前页变化信号 |
+
+#### 用法示例
+
+**例 37_qstackedwidget 列表栏多页面切换**:左边列表、右边堆叠页,靠信号槽联动。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QStackedWidget>
+#include <QHBoxLayout>
+#include <QListWidget>
+#include <QLabel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* widget小部件 */
+    QWidget *widget;
+    /* 水平布局 */
+    QHBoxLayout *hBoxLayout;
+    /* 列表视图 */
+    QListWidget *listWidget;
+    /* 堆栈窗口部件 */
+    QStackedWidget *stackedWidget;
+    /* 3个标签 */
+    QLabel *label[3];
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    this->setGeometry(0, 0, 800, 480);
+
+    /* widget小部件实例化 */
+    widget = new QWidget(this);
+
+    /* 设置居中 */
+    this->setCentralWidget(widget);
+
+    /* 水平布局实例化 */
+    hBoxLayout = new QHBoxLayout();
+
+    /* 堆栈部件实例化 */
+    stackedWidget = new QStackedWidget();
+
+    /* 列表实例化 */
+    listWidget = new QListWidget();
+
+    QList <QString>strListWidgetList;
+    strListWidgetList<<"窗口一"<<"窗口二"<<"窗口三";
+
+    for (int i = 0; i < 3; i++){
+        /* listWidget插入项 */
+        listWidget->insertItem(
+                    i,
+                    strListWidgetList[i]
+                    );
+    }
+
+    QList <QString>strLabelList;
+    strLabelList<<"标签一"<<"标签二"<<"标签三";
+
+    for (int i = 0; i < 3; i++){
+        label[i] = new QLabel();
+        /* 设置标签文本 */
+        label[i]->setText(strLabelList[i]);
+        /* 标签对齐方式(居中) */
+        label[i]->setAlignment(Qt::AlignCenter);
+        /* 添加页面 */
+        stackedWidget->addWidget(label[i]);
+    }
+
+    /* 设置列表的最大宽度 */
+    listWidget->setMaximumWidth(200);
+    /* 添加到水平布局 */
+    hBoxLayout->addWidget(listWidget);
+    hBoxLayout->addWidget(stackedWidget);
+
+    /* 将widget的布局设置成hBoxLayout */
+    widget->setLayout(hBoxLayout);
+
+    /* 利用listWidget的信号currentRowChanged()与
+     * 槽setCurrentIndex(),进行信号与槽连接
+     */
+    connect(listWidget, SIGNAL(currentRowChanged(int)),
+            stackedWidget, SLOT(setCurrentIndex(int)));
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+点击左侧列表的“窗口一/二/三”,右侧堆叠页随之切换显示对应标签。这是同一份代码里“信号 → 槽”跨对象直连的典型用法。
+
+---
+
+### 3.6 QWidget
+
+#### 控件简介
+
+`QWidget` 类是所有用户界面对象的基类(如 `QLabel` 继承 `QFrame`,`QFrame` 又继承 `QWidget`)。Widget 是用户界面的基本单元:它从窗口系统接收鼠标、键盘和其他事件,并在屏幕上绘制自己。每个 Widget 都是矩形,按 **Z-order** 排序。
+
+Z-order 指沿 z 轴(垂直于屏幕)的堆叠顺序。GUI 窗口可能重叠,当两个窗口重叠时,Z 顺序决定谁在上。就像桌面上的一叠纸:每张纸是一个窗口,桌面是屏幕,最上面的窗口 z 值最高。
+
+`QWidget` 不是抽象类,可以用作其他 Widget 的容器,也容易子类化来创建定制 Widget。它属性方法相当多,初学者通常只用它作容器并接收事件。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setLayout(QLayout*)` | 设置布局 |
+| `setGeometry(x, y, w, h)` | 位置与大小 |
+| `show()` / `hide()` / `close()` | 显示/隐藏/关闭 |
+| `setStyleSheet(QString)` | 样式表 |
+| `setWindowTitle(QString)` | 窗口标题 |
+| `setAttribute(Qt::WidgetAttribute)` | 设置属性(如 `WA_DeleteOnClose`) |
+| `setSizePolicy(QSizePolicy::Policy h, v)` | 尺寸策略 |
+| `update()` | 请求重绘 |
+
+#### 用法示例
+
+`QWidget` 在 1.1 节的例 29 中已经作为容器使用:`hWidget`、`vWidget` 都是 `QWidget`,通过 `setLayout()` 承载布局。下面再给一个最小片段,说明“容器 + 布局”的标准写法:
+
+```cpp
+/* 新建一个容器部件 */
+QWidget *container = new QWidget(this);
+
+/* 新建布局并添加子部件 */
+QVBoxLayout *layout = new QVBoxLayout();
+layout->addWidget(new QPushButton("按钮一"));
+layout->addWidget(new QPushButton("按钮二"));
+
+/* 把布局交给容器,容器再作为主窗口中央部件 */
+container->setLayout(layout);
+this->setCentralWidget(container);
+```
+
+#### 运行效果说明
+
+`QWidget` 本身没有可见外观,它的作用是承载布局与其他控件;把布局设置给容器后,子控件会随容器尺寸变化自动排布。
+
+---
+
+### 3.7 QMdiArea
+
+#### 控件简介
+
+`QMdiArea` 继承 `QAbstractScrollArea`,提供一个显示 MDI(多文档界面)窗口的区域,功能本质上类似 MDI 窗口管理器。复杂程序常使用 MDI 框架。通过 `addSubWindow()` 添加 `QMdiSubWindow` 子窗口。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addSubWindow(QWidget *widget)` | 添加子窗口 |
+| `removeSubWindow(QWidget*)` | 移除子窗口 |
+| `setActiveSubWindow(QMdiSubWindow*)` | 激活子窗口 |
+| `activeSubWindow()` | 当前活动子窗口 |
+| `subWindowList()` | 子窗口列表 |
+| `cascadeSubWindows()` | 级联排列 |
+| `tileSubWindows()` | 平铺排列 |
+| `closeAllSubWindows()` | 关闭全部子窗口 |
+
+#### 用法示例
+
+**例 38_qmdiarea 父子窗口**:每点按钮新建一个子窗口。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QMdiSubWindow>
+#include <QMdiArea>
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* Mdi Area区域对象 */
+    QMdiArea *mdiArea;
+    /* MdiSubWindow子窗口对象 */
+    QMdiSubWindow *newMdiSubWindow;
+    /* 用作点击创建新的窗口 */
+    QPushButton *pushButton;
+
+private slots:
+    /* 按钮槽函数 */
+    void creat_newMdiSubWindow();
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置窗口的显示位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+    pushButton = new QPushButton("新建窗口", this);
+    pushButton->setGeometry(0, 30, 100, 30);
+
+    mdiArea = new QMdiArea(this);
+    /* 设置MDI Area区域大小 */
+    mdiArea->setGeometry(100, 30, 700, 430);
+    /* 连接信号槽 */
+    connect(pushButton, SIGNAL(clicked()),
+            this, SLOT(creat_newMdiSubWindow()));
+}
+
+void MainWindow::creat_newMdiSubWindow()
+{
+    newMdiSubWindow = new QMdiSubWindow();
+    newMdiSubWindow->setWindowTitle("新建窗口");
+
+    /* 如果窗口设置了Qt::WA_DeleteOnClose这个属性,
+     * 在窗口接受了关闭事件后,Qt会释放这个窗口所占用的资源
+     */
+    newMdiSubWindow->setAttribute(Qt::WA_DeleteOnClose);
+
+    /* 添加子窗口 */
+    mdiArea->addSubWindow(newMdiSubWindow);
+    /* 显示窗口,不设置时为不显示 */
+    newMdiSubWindow->show();
+    /* 以平铺方式排列所有窗口 */
+    mdiArea->tileSubWindows();
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+> 子窗口内容默认为空,可用 `newMdiSubWindow->setWidget(QWidget*)` 放入 `QLineEdit` 等部件。另有 `cascadeSubWindows()` 以级联方式排列。
+
+#### 运行效果说明
+
+每点一次“新建窗口”,MDI 区域里新增一个标题为“新建窗口”的子窗口,并自动平铺排列;关闭子窗口时因 `WA_DeleteOnClose` 会释放资源。
+
+---
+
+### 3.8 QDockWidget
+
+#### 控件简介
+
+`QDockWidget` 继承 `QWidget`,提供一个可以停靠在 `QMainWindow` 内、也可作为桌面顶级窗口浮动的部件,也称工具面板或实用程序窗口。停靠窗口可被移动到当前区域、新区域,或由终端用户浮动/取消浮动。API 允许限制其移动、浮动、关闭能力及可放置区域。
+
+初始停靠区域:`Qt::BottomDockWidgetArea`(底部)、`Qt::LeftDockWidgetArea`(左)、`Qt::RightDockWidgetArea`(右)、`Qt::TopDockWidgetArea`(顶)、`Qt::NoDockWidgetArea`(不显示)。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QDockWidget(const QString &title, QWidget *parent)` | 构造:标题 + 父对象 |
+| `setWidget(QWidget*)` | 设置内部部件 |
+| `QMainWindow::addDockWidget(area, dock)` | 添加到主窗口指定停靠区 |
+| `setAllowedAreas(Qt::DockWidgetAreas)` | 允许停靠的区域 |
+| `setFloating(bool)` | 是否浮动 |
+| `setFeatures(Features)` | 可移动/可关闭/可浮动等特性 |
+| `visibilityChanged(bool)` | 可见性变化信号 |
+
+#### 用法示例
+
+**例 39_qdockwidget 停靠窗口**:文本编辑框放入停靠窗口,停靠在主窗口顶部。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QDockWidget>
+#include <QTextEdit>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 停靠窗口部件 */
+    QDockWidget *dockWidget;
+    /* 文本编辑框 */
+    QTextEdit *textEdit;
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体的显示的位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化标题为停靠窗口 */
+    dockWidget = new QDockWidget("停靠窗口", this);
+
+    /* 实例化文本编辑框 */
+    textEdit = new QTextEdit(dockWidget);
+
+    textEdit->setText("这是一个测试");
+
+    /* 停靠窗口添加文本编辑框 */
+    dockWidget->setWidget(textEdit);
+
+    /* 放在主窗体的顶部 */
+    this->addDockWidget(Qt::TopDockWidgetArea, dockWidget);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+停靠窗口出现在主窗口顶部,带有标题“停靠窗口”,内部是一个显示“这是一个测试”的文本编辑框;拖动它可移动并重新停靠。
+
+---
+
+## 4. 项目视图组(基于模型)
+
+基于模型的视图需要先建立 Model,再设置给视图。下面依次讲解 `QListView`、`QTreeView`、`QTableView`、`QColumnView`、`QUndoView`。
+
+### 4.1 QListView
+
+#### 控件简介
+
+`QListView` 继承 `QAbstractItemView`,被 `QListWidget` 和 `QUndoView` 继承。它以简单的非分层列表或图标集合形式显示存储在模型中的项,使用 Qt 的模型/视图体系结构,比旧的 `QListBox`、`QIconView` 更灵活。`QListView` 基于 Model,需要建模,减少数据冗余、效率高。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setModel(QAbstractItemModel*)` | 设置模型 |
+| `setViewMode(QListView::ViewMode)` | `ListMode` / `IconMode` |
+| `setDragEnabled(bool)` | 是否可拖动 |
+| `setSelectionMode(QAbstractItemView::SelectionMode)` | 选择模式 |
+| `setModelColumn(int)` | 显示模型哪一列 |
+| `clicked(QModelIndex)` / `doubleClicked(QModelIndex)` | 点击信号 |
+
+#### 用法示例
+
+**例 40_qlistview 清单图**:用 `QStringListModel` 提供数据。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QListView>
+#include <QStringListModel>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* QListView对象 */
+    QListView *listView;
+    /* 字符串模型对象 */
+    QStringListModel *stringListModel;
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗口位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化 */
+    listView = new QListView(this);
+    /* 将listView居中 */
+    setCentralWidget(listView);
+
+    QStringList strList;
+    strList<<"高三(1)班"<<"高三(2)班"<<"高三(3)班";
+
+    /* 实例化,字符串模型 */
+    stringListModel = new QStringListModel(strList);
+
+    /* 向表中插入一段数据 */
+    listView->setModel(stringListModel);
+    /* 设置为视图为图标模式 */
+    listView->setViewMode(QListView::IconMode);
+    /* 设置为不可拖动 */
+    listView->setDragEnabled(false);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+三个班级以图标模式显示。双击项可修改内容,也可删除。
+
+---
+
+### 4.2 QTreeView
+
+#### 控件简介
+
+`QTreeView` 继承 `QAbstractItemView`,被 `QTreeWidget` 继承。它提供树视图的默认模型/视图实现,实现模型项的树表示。要显示数据需构造 Model 并设置给 `QTreeView`,最常用的是 `QStandardItemModel`。表头内容也由 Model 管理,`setHorizontalHeaderLabels()` 可设置列数与每列文字。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setModel(QAbstractItemModel*)` | 设置模型 |
+| `setHeaderHidden(bool)` | 是否隐藏表头 |
+| `expandAll()` / `collapseAll()` | 展开/折叠全部 |
+| `setColumnWidth(int, int)` | 列宽 |
+| `QStandardItemModel::appendRow(...)` | 追加行 |
+| `QStandardItem::appendRow(...)` | 给某项追加子行 |
+| `clicked(QModelIndex)` / `expanded(QModelIndex)` | 信号 |
+
+#### 用法示例
+
+**例 41_qtreeview 仿 Word 标题**:一级标题下挂二级标题。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QTreeView>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    QTreeView  *treeView;
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+#include <QStandardItemModel>
+#include <QStandardItem>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置窗口的位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+    /* 实例化QTreeView对象 */
+    treeView = new QTreeView(this);
+    /* 居中 */
+    setCentralWidget(treeView);
+
+    /* 构建Model */
+    QStandardItemModel *sdiModel = new QStandardItemModel(treeView);
+    sdiModel->setHorizontalHeaderLabels(
+                QStringList()<<QStringLiteral("标题")
+                << QStringLiteral("名称")
+                );
+
+    for(int i = 0; i < 5; i++) {
+        /* 一级标题 */
+        QList<QStandardItem*> items1;
+        QStandardItem* item1 =
+                new QStandardItem(QString::number(i));
+        QStandardItem* item2 =
+                new QStandardItem(QStringLiteral("一级标题"));
+        /* 添加项一 */
+        items1.append(item1);
+        /* 添加项二 */
+        items1.append(item2);
+        /* appendRow方法添加到model上 */
+        sdiModel->appendRow(items1);
+
+        for(int j = 0; j < 5; j++) {
+            /* 在一级标题后面插入二级标题 */
+            QList<QStandardItem*> items2;
+            QStandardItem* item3 =
+                    new QStandardItem(QString::number(j));
+            QStandardItem* item4 =
+                    new QStandardItem(QStringLiteral("二级标题"));
+            items2.append(item3);
+            items2.append(item4);
+            /* 使用appendRow方法添加到item1上 */
+            item1->appendRow(items2);
+        }
+    }
+    /* 设置Model给treeView */
+    treeView->setModel(sdiModel);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+显示两列表头“标题”“名称”,5 个一级标题,每个下面 5 个二级标题;点击数字前的展开箭头可展开二级标题。
+
+---
+
+### 4.3 QTableView
+
+#### 控件简介
+
+`QTableView` 继承 `QAbstractItemView`,被 `QTableWidget` 继承。它提供表视图的默认模型/视图实现,用于显示来自模型的数据。同样需要构造 Model(最常用 `QStandardItemModel`),表头内容也由 Model 管理。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setModel(QAbstractItemModel*)` | 设置模型 |
+| `setShowGrid(bool)` | 显示网格线 |
+| `horizontalHeader()` / `verticalHeader()` | 水平/垂直表头 |
+| `QHeaderView::setSectionResizeMode(mode)` | 表头缩放模式(`Stretch` 平均分) |
+| `setSelectionBehavior(...)` | 选择行为 |
+| `setEditTriggers(...)` | 编辑触发方式 |
+| `QStandardItemModel::setItem(row, col, item)` | 设置单元格 |
+
+#### 用法示例
+
+**例 42_qtableview 表格视图**:构建一个成绩表。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QTableView>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    QTableView *tableView;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include <QStandardItemModel>
+#include <QHeaderView>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置窗口的位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+    tableView = new QTableView(this);
+    setCentralWidget(tableView);
+    /* 显示网格线 */
+    tableView->setShowGrid(true);
+
+    QStandardItemModel* model = new QStandardItemModel();
+    QStringList labels =
+            QObject::tr("语文,数学,英语").simplified().split(",");
+    /* 设置水平头标签 */
+    model->setHorizontalHeaderLabels(labels);
+
+    /* item */
+    QStandardItem* item = 0;
+    /* model插入项内容 */
+    for(int i = 0; i < 5; i++){
+        item = new QStandardItem("80");
+        model->setItem(i, 0, item);
+        item = new QStandardItem("99");
+        model->setItem(i, 1, item);
+        item = new QStandardItem("100");
+        model->setItem(i, 2, item);
+    }
+    /* 将model设置给tableView */
+    tableView->setModel(model);
+    /* 平均分列 */
+    tableView->horizontalHeader()
+            ->setSectionResizeMode(QHeaderView::Stretch);
+    /* 平均分行 */
+    tableView->verticalHeader()
+            ->setSectionResizeMode(QHeaderView::Stretch);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+生成 5 行 3 列的成绩表,列头为“语文、数学、英语”,行列均等高/等宽铺满窗口。
+
+---
+
+### 4.4 QColumnView
+
+#### 控件简介
+
+`QColumnView` 继承 `QAbstractItemView`,在许多 `QListView` 中显示一个模型,每个 `QListView` 对应树中的一个层次结构,有时称为级联列表。它是模型/视图框架的一部分,实现 `QAbstractItemView` 定义的接口,以显示派生自 `QAbstractItemModel` 的模型数据。填写收货地址的多级选择就是这种效果。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setModel(QAbstractItemModel*)` | 设置模型 |
+| `setColumnWidths(QList<int>)` | 各列宽度 |
+| `updatePreviewWidget(QModelIndex)` | 更新预览部件 |
+| `setResizeGripsVisible(bool)` | 是否显示拖动柄 |
+
+#### 用法示例
+
+**例 43_qcolumnview 收货地址**:省 → 市 → 镇 多级 `QStandardItem`。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QColumnView>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    QColumnView *columnView;
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include <QStandardItem>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体显示位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+    QStandardItemModel *model = new QStandardItemModel;
+
+    /* 省份 */
+    QStandardItem *province = new QStandardItem("广东省");
+
+    /* 城市 */
+    QStandardItem *city1 = new QStandardItem("茂名市");
+    QStandardItem *city2 = new QStandardItem("中山市");
+
+    /* 添加城市到省份下 */
+    province->appendRow(city1);
+    province->appendRow(city2);
+
+    QStandardItem *town1 = new QStandardItem("电白镇");
+    QStandardItem *town2 = new QStandardItem("南头镇");
+
+    /* 添加城镇到城市下 */
+    city1->appendRow(town1);
+    city2->appendRow(town2);
+
+    columnView = new QColumnView;
+
+    /* 建立model */
+    model->appendRow(province);
+
+    /* 设置model */
+    columnView->setModel(model);
+
+    /* 设置居中 */
+    setCentralWidget(columnView);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+第一列显示“广东省”,点它后第二列出现“茂名市、中山市”,点城市后第三列出现对应城镇。
+
+---
+
+### 4.5 QUndoView
+
+#### 控件简介
+
+`QUndoView` 继承 `QListView`,显示 `QUndoStack` 的内容——即撤销堆栈上已推入的命令列表。它总是选中最近执行的命令;选择不同命令会调用 `QUndoStack::setIndex()`,把文档状态向前或向后滚动到该命令。可用 `setStack()` 显式设置堆栈,或用 `setGroup()` 设置 `QUndoGroup`(组的活动堆栈变化时视图自动更新)。效果类似 Photoshop 的历史记录面板。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `QUndoView(QUndoStack *stack, QWidget *parent)` | 用撤销栈构造 |
+| `setStack(QUndoStack*)` / `setGroup(QUndoGroup*)` | 设置栈/组 |
+| `setEmptyLabel(QString)` | 空栈时显示文本 |
+| `QUndoStack::push(QUndoCommand*)` | 入栈(会自动调用命令的 `redo()`) |
+| `QUndoStack::undo()` / `redo()` | 撤销/重做 |
+| `QUndoStack::createUndoAction()` / `createRedoAction()` | 生成撤销/重做动作 |
+| `QUndoStack::indexChanged(int)` | 索引变化信号 |
+| `QUndoCommand::redo()` / `undo()` / `setText(QString)` | 重写/设置命令描述 |
+
+#### 用法示例
+
+**例 44_qundoview 仿 PS 历史记录**:自定义 `QUndoCommand`,实现“加 1”的可撤销操作。需要额外新建 `command.h`、`command.cpp`。
+
+`command.h`:
+
+```cpp
+#ifndef COMMAND_H
+#define COMMAND_H
+
+#include <QUndoCommand>
+#include <QObject>
+
+class addCommand : public QUndoCommand
+{
+public:
+    addCommand(int *value, QUndoCommand* parent = 0);
+    ~addCommand();
+
+    /* 重写重做与撤回方法 */
+    void redo() override;
+    void undo() override;
+
+private:
+    /* 新的count */
+    int *new_count;
+
+    /* 旧的count */
+    int old_count;
+};
+
+#endif // COMMAND_H
+```
+
+`command.cpp`:
+
+```cpp
+#include "command.h"
+#include <QDebug>
+
+addCommand::addCommand(int *value, QUndoCommand *parent)
+{
+    /* 使用Q_UNUSED,避免未使用的数据类型 */
+    Q_UNUSED(parent);
+
+    /* undoView显示的操作信息 */
+    setText("进行了加1操作");
+
+    /* value的地址赋值给new_count */
+    new_count = value;
+
+    /* 让构造函数传过来的*new_count的值赋值给old_count */
+    old_count = *new_count;
+}
+
+/* 执行stack push时或者重做操作时会自动调用 */
+void addCommand::redo()
+{
+    /* 重新赋值给new_count */
+    *new_count = old_count;
+
+    /* 打印出*new_count的值 */
+    qDebug()<<"redo:"<<*new_count<<endl;
+}
+
+/* 回撤操作时执行 */
+void addCommand::undo()
+{
+    /* 回撤操作每次应减一 */
+    (*new_count)--;
+
+    /* 打印出*new_count的值 */
+    qDebug()<<"undo:"<<*new_count<<endl;
+}
+
+addCommand::~addCommand()
+{
+}
+```
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QUndoView>
+#include <QUndoStack>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <QPushButton>
+#include <QLabel>
+#include "command.h"
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 水平布局 */
+    QHBoxLayout *hLayout;
+    /* 垂直布局 */
+    QVBoxLayout *vLayout;
+    /* 用于容纳hLayout布局 */
+    QWidget *mainWidget;
+    /* 容器QWidget,用于容纳标签与按钮 */
+    QWidget *widget;
+    /* 存放 QUndoCommand命令的栈 */
+    QUndoStack *undoStack;
+    /* 历史记录面板 */
+    QUndoView *undoView;
+    /* 用于显示计算结果 */
+    QLabel *label;
+    /* 按钮 */
+    QPushButton *pushButton;
+    /* 计算结果 */
+    int count;
+
+private slots:
+    void pushButtonClieked();
+    void showCountValue(int);
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include <QDebug>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体显示的位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例一个水平布局,用于左侧按钮区域与右侧历史记录面板 */
+    hLayout = new QHBoxLayout();
+
+    /* 实例一个垂直布局,用于左侧标签与按钮 */
+    vLayout = new QVBoxLayout();
+
+    /* 主Widget,因为MainWindow自带一个布局,
+     * 我们要新建一个Widget容纳新布局
+     */
+    mainWidget = new QWidget();
+
+    /* 用于存放命令行栈 */
+    undoStack = new QUndoStack(this);
+
+    /* 用于容纳左侧标签与按钮布局 */
+    widget = new QWidget();
+
+    /* 历史记录面板实例化 */
+    undoView = new QUndoView(undoStack);
+
+    /* 实例化一个按钮,用于加一操作 */
+    pushButton = new QPushButton();
+
+    /* 标签,用于显示计算结果 */
+    label = new QLabel();
+
+    /* 设置widget的大小 */
+    widget->setMinimumSize(400, 480);
+
+    /* 将两个widget添加到水平布局 */
+    hLayout->addWidget(widget);
+    hLayout->addWidget(undoView);
+
+    /* 初始化count的值 */
+    count = 0;
+
+    /* 显示初始化计算结果 */
+    label->setText("计算结果:" + QString::number(count));
+    label->setAlignment(Qt::AlignCenter);
+
+    /* 左侧布局 */
+    vLayout->addWidget(label);
+    vLayout->addWidget(pushButton);
+
+    /* 左侧布局控件的高度设置 */
+    label->setMaximumHeight(this->height() / 5);
+    pushButton->setMaximumHeight(this->height() / 5);
+
+    /* 按钮文本设置 */
+    pushButton->setText("加1");
+
+    /* 设置widget的布局为vLayout */
+    widget->setLayout(vLayout);
+
+    /* 将主窗体的布局设置为hLayout */
+    mainWidget->setLayout(hLayout);
+
+    /* 设置mainWidget为主窗体的居中widget */
+    this->setCentralWidget(mainWidget);
+
+    /* 信号槽连接,按钮点击,执行加一操作 */
+    connect(pushButton, SIGNAL(clicked()), this,
+            SLOT(pushButtonClieked()));
+
+    /* 信号槽连接,历史记录项index发生变化,显示count大小 */
+    connect(undoStack, SIGNAL(indexChanged(int) ),
+            this, SLOT(showCountValue(int)));
+}
+
+/* 入栈操作会自动调用addCommand的redo */
+void MainWindow::pushButtonClieked()
+{
+    /* 变量值加一 */
+    count++;
+
+    /* value指向count的地址 */
+    int *value = &count;
+
+    /* 用重写的addCommand类实例化 */
+    QUndoCommand *add = new addCommand(value);
+
+    /* 入栈 */
+    undoStack->push(add);
+}
+
+void MainWindow::showCountValue(int)
+{
+    /* 标签用于显示计算结果 */
+    label->setText("计算结果:" + QString::number(count));
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+点击“加 1”按钮,计算结果加 1;用方向键或鼠标选择右侧历史记录项,会执行重做/回撤,输出窗口打印 `redo:`/`undo:` 信息,计算结果回到该步数值。
+
+> `command.h` / `command.cpp` 的创建方法:项目右键 `Add New…` → C++ → `C++ Source File`,分别命名 `command.h`、`command.cpp`,用于重写 `QUndoCommand`。
+
+---
+
+## 5. 项目控件组(基于项)
+
+基于项的控件是“自带内部模型的视图”,名字与视图组控件相似(如 `QListWidget` 继承 `QListView`)。一般处理大数据时用基于模型的视图,中小数据用基于项的控件更省事。
+
+### 5.1 QListWidget
+
+#### 控件简介
+
+`QListWidget` 继承 `QListView`,提供基于项的列表小部件。它用内部模型管理列表中的每个 `QListWidgetItem`,直接 `addItem` 即可添加项目(图标、文字),操作方便。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addItem(const QString&)` / `addItem(QListWidgetItem*)` | 添加项 |
+| `insertItem(int row, const QString&)` | 插入项 |
+| `takeItem(int row)` | 移除并返回项 |
+| `item(int row)` | 取项 |
+| `count()` | 项数 |
+| `clear()` | 清空 |
+| `currentRowChanged(int)` | 当前行变化信号 |
+| `itemClicked(QListWidgetItem*)` / `itemDoubleClicked(...)` | 点击/双击信号 |
+
+#### 用法示例
+
+**例 45_qlistwidget 添加“歌曲”**:打开文件对话框,把选中的 mp3 加到列表。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QListWidget>
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 声明对象 */
+    QListWidget *listWidget;
+    QPushButton *pushButton;
+
+private slots:
+    void pushButtonClicked();
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+#include "QFileDialog"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗口的显示位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    listWidget = new QListWidget(this);
+
+    /* 设置listWidget的大小 */
+    listWidget->setGeometry(0, 0, 480, 480);
+
+    listWidget->addItem("请单击右边的添加项添加内容");
+
+    pushButton = new QPushButton(this);
+
+    /* 设置pushButton的位置与大小 */
+    pushButton->setGeometry(540, 200, 200, 100);
+    pushButton->setText("添加项");
+
+    /* 信号与槽连接 */
+    connect(pushButton, SIGNAL(clicked()),
+            this, SLOT(pushButtonClicked()));
+}
+
+void MainWindow::pushButtonClicked()
+{
+    /* 调用系统打开文件窗口,设置窗口标题为“添加项”,过滤文件名 */
+    QString fileName = QFileDialog::getOpenFileName(
+                this,tr("添加项"),"",
+                tr("Files(*.mp3)")
+                );
+
+    /* 判断是否选中打开mp3文件 */
+    if (fileName != NULL)
+        /* 添加项到列表中 */
+        listWidget->addItem(fileName);
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+> 可在项目下用 `touch 0.mp3 1.mp3` 创建两个后缀为 mp3 的文件用于测试(并非真正歌曲)。本例就是音乐播放器“打开歌曲”的部分代码。
+
+#### 运行效果说明
+
+点击“添加项”弹出文件对话框,只显示 mp3 文件;选择一个加入左侧列表。可自行扩展删除项、删除全部等按钮。
+
+---
+
+### 5.2 QTreeWidget
+
+#### 控件简介
+
+`QTreeWidget` 继承 `QTreeView`,提供使用预定义树模型的树视图。它基于模型/视图体系结构,使用默认模型保存项,每个项都是 `QTreeWidgetItem`。下例模拟飞信联系人分组,演示树节点与子节点的联动选择。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `addTopLevelItem(QTreeWidgetItem*)` | 添加顶层项 |
+| `QTreeWidgetItem(QTreeWidget*)` / `QTreeWidgetItem(QTreeWidgetItem*)` | 构造顶层项/子项 |
+| `setText(int col, QString)` / `text(int)` | 设置/读取文本 |
+| `setCheckState(int col, Qt::CheckState)` / `checkState(int)` | 设置/读取勾选状态 |
+| `setFlags(Qt::ItemFlags)` | 项属性(可勾选、可用、可选) |
+| `childCount()` / `child(int)` | 子项数量/子项 |
+| `parent()` | 父项 |
+| `itemChanged(QTreeWidgetItem*, int)` | 项变化信号 |
+| `clear()` | 清空 |
+
+#### 用法示例
+
+**例 46_qtreewidget 群发信息**:选中顶层节点则子节点全选;子节点部分选中则顶层显示半选。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QTreeWidget>
+#include <QTreeWidgetItem>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* QTreeWidget对象 */
+    QTreeWidget *treeWidget;
+    /* 顶层树节点 */
+    QTreeWidgetItem *parentItem;
+    /* 声明三个子节点 */
+    QTreeWidgetItem *subItem[3];
+
+    /* 子节点处理函数 */
+    void updateParentItem(QTreeWidgetItem*);
+
+private slots:
+    /* 槽函数 */
+    void treeItemChanged(QTreeWidgetItem*, int);
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化 */
+    treeWidget = new QTreeWidget(this);
+
+    /* 居中 */
+    setCentralWidget(treeWidget);
+
+    /* 清空列表 */
+    treeWidget->clear();
+
+    /* 实例化顶层树节点 */
+    parentItem = new QTreeWidgetItem(treeWidget);
+    parentItem->setText(0, "同事");
+
+    parentItem->setFlags(
+                Qt::ItemIsUserCheckable
+                | Qt::ItemIsEnabled
+                | Qt::ItemIsSelectable
+                );
+    /* 树节点设置为未选中 */
+    parentItem->setCheckState(0, Qt::Unchecked);
+
+    /* 字符串链表 */
+    QList <QString> strList;
+    strList<<"关羽"<<"刘备"<<"张飞";
+
+    for (int i = 0; i < 3; i++){
+        /* 实例化子节点 */
+        subItem[i] = new QTreeWidgetItem(parentItem);
+        /* 设置子节点的文本,参数0代表第0列 */
+        subItem[i]->setText(0, strList[i]);
+        /* 设置子节点的属性为用户可选、项开启、项可选 */
+        subItem[i]->setFlags(
+                    Qt::ItemIsUserCheckable
+                    | Qt::ItemIsEnabled
+                    | Qt::ItemIsSelectable
+                    );
+        /* 设置子节点的状态为未选中 */
+        subItem[i]->setCheckState(0,Qt::Unchecked);
+    }
+    /* 信号槽连接 */
+    connect(treeWidget,SIGNAL(itemChanged(QTreeWidgetItem* , int)),
+            this, SLOT(treeItemChanged(QTreeWidgetItem* , int)));
+
+}
+
+/* 更新树节点函数 */
+void MainWindow::updateParentItem(QTreeWidgetItem *item)
+{
+    /* 获取子节点的父节点(树节点) */
+    QTreeWidgetItem* parent = item->parent();
+    if(parent == NULL){
+        return;
+    }
+    /* 初始化选中的数目为0,下面根据selectCount来判断树节点的状态 */
+    int selectCount = 0;
+    /* 获取树节点的子节点总数 */
+    int childCount = parent->childCount();
+    /* 循环判断子节点的状态 */
+    for(int i = 0; i < childCount; i ++){
+        QTreeWidgetItem* childItem =parent->child(i);
+        /* 判断当前子节点的状态是否为选中状态,如果是,则加一 */
+        if(childItem->checkState(0) == Qt::Checked) {
+            selectCount ++;
+        }
+    }
+    /* 根据selectCount来判断树节点的状态 */
+    if (selectCount <= 0) {
+        /* 未选中任何子节点:树节点为未选中状态 */
+        parent->setCheckState(0, Qt::Unchecked);
+    } else if (selectCount > 0 && selectCount < childCount) {
+        /* 部分选中时,树节点为半选状态 */
+        parent->setCheckState(0, Qt::PartiallyChecked);
+    } else if (selectCount == childCount){
+        /* 子节点全选时,树节点为选中状态 */
+        parent->setCheckState(0, Qt::Checked);
+    }
+}
+
+void MainWindow::treeItemChanged(QTreeWidgetItem *item, int)
+{
+    /* 获取子节点总数 */
+    int count = item->childCount();
+
+    /* 若顶层树节点选中 */
+    if(Qt::Checked == item->checkState(0) ) {
+        /* 若选中的项是树节点,count会大于0,否则选中的项是子节点 */
+        if (count > 0) {
+            for (int i = 0; i < count; i++) {
+                /* 子节点全选 */
+                item->child(i)->setCheckState(0, Qt::Checked);
+            }
+        } else {
+            /* 子节点处理 */
+            updateParentItem(item);
+        }
+        /* 若顶层树节点取消选中时 */
+    } else if (Qt::Unchecked == item->checkState(0)) {
+        if (count > 0){
+            for (int i = 0; i < count; i++) {
+                /* 子节点全不选 */
+                item->child(i)->setCheckState(0, Qt::Unchecked);
+            }
+        } else {
+            /* 子节点处理 */
+            updateParentItem(item);
+        }
+    }
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+勾选“同事”则“关羽、刘备、张飞”全部勾选;取消勾选则全部取消;只勾选部分子节点时,“同事”显示为半选状态。这正是群发信息选择联系人的交互。
+
+---
+
+### 5.3 QTableWidget
+
+#### 控件简介
+
+`QTableWidget` 继承 `QTableView`,提供带默认模型的基于项的表视图。表中的项由 `QTableWidgetItem` 提供。它适合直接绘制小表格,双击即可编辑项内容。
+
+#### 常用属性与信号
+
+| 成员 | 说明 |
+| ---- | ---- |
+| `setColumnCount(int)` / `setRowCount(int)` | 设置列数/行数 |
+| `setHorizontalHeaderLabels(QStringList)` | 设置水平表头 |
+| `setVerticalHeaderLabels(QStringList)` | 设置垂直表头 |
+| `setItem(int row, int col, QTableWidgetItem*)` | 设置单元格 |
+| `item(int row, int col)` | 取单元格 |
+| `QTableWidgetItem(const QString&)` | 构造项 |
+| `setTextAlignment(Qt::Alignment)` | 文本对齐 |
+| `cellClicked(int, int)` / `cellChanged(int, int)` | 单元格信号 |
+
+#### 用法示例
+
+**例 47_qtablewidget 表格**:绘制一个“姓名/性别”两行两列表格。
+
+`mainwindow.h`:
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QTableWidget>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* QTableWidget表格 */
+    QTableWidget *tableWidget;
+
+    /* QTableWidgetItem表格数据(项) */
+    QTableWidgetItem *tableWidgetItem[4];
+
+};
+#endif // MAINWINDOW_H
+```
+
+`mainwindow.cpp`:
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体的大小与位置 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化 */
+    tableWidget = new QTableWidget(this);
+    /* 设置tableWidget表居中 */
+    setCentralWidget(tableWidget);
+    /* 设置列数 */
+    tableWidget->setColumnCount(2);
+    /* 设置行数 */
+    tableWidget->setRowCount(2);
+    /* 使用标签设置水平标题标签 */
+    tableWidget->setHorizontalHeaderLabels(
+                QStringList()<<"姓名"<<"性别"
+                );
+
+    /* 字符串类型链表 */
+    QList <QString> strList;
+    strList<<"小明"<<"小红"<<"男"<<"女";
+
+    for (int i = 0; i < 4; i++) {
+        /* 实例化 */
+        tableWidgetItem[i] = new QTableWidgetItem(strList[i]);
+        /* 设置文本居中对齐 */
+        tableWidgetItem[i]->setTextAlignment(Qt::AlignCenter);
+    }
+    /* 插入数据,表的index就是一个二维数组数据 */
+    tableWidget->setItem(0, 0, tableWidgetItem[0]);
+    tableWidget->setItem(1, 0, tableWidgetItem[1]);
+    tableWidget->setItem(0, 1, tableWidgetItem[2]);
+    tableWidget->setItem(1, 1, tableWidgetItem[3]);
+
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+#### 运行效果说明
+
+生成 2×2 表格,表头“姓名/性别”,内容“小明/男”“小红/女”居中对齐。双击单元格可修改内容,也可删除内容。
+
+---
+
+## 6. 总结
+
+### 6.1 布局选择速查
+
+| 需求 | 首选 |
+| ---- | ---- |
+| 一行/一列排列 | `QHBoxLayout` / `QVBoxLayout` |
+| 多行多列、需要行列比例 | `QGridLayout` |
+| 少量“标签 + 输入框”表单 | `QFormLayout` |
+| 布局中留空白/顶开 | `QSpacerItem` 或 `addStretch()` |
+| 页面切换(页签) | `QTabWidget` |
+| 页面切换(列表联动) | `QListWidget` + `QStackedWidget` |
+| 折叠面板 | `QToolBox` |
+| 多窗口工作区 | `QMdiArea` |
+| 可拖动工具面板 | `QDockWidget` |
+| 大内容滚动查看 | `QScrollArea` |
+
+### 6.2 基于模型 vs 基于项
+
+```mermaid
+flowchart LR
+    subgraph M["基于模型 Model/View"]
+        M1["QListView<br/>QTreeView<br/>QTableView<br/>QColumnView"]
+        M2["需自建 Model<br/>QStringListModel / QStandardItemModel"]
+        M1 --- M2
+    end
+    subgraph I["基于项 Item"]
+        I1["QListWidget<br/>QTreeWidget<br/>QTableWidget"]
+        I2["自带内部模型<br/>QListWidgetItem 等"]
+        I1 --- I2
+    end
+    M -->|"数据量大、结构复杂"| V["选型"]
+    I -->|"数据量小、开发快"| V
+```
+
+| 对比项 | 基于模型(View) | 基于项(Widget) |
+| ------ | ---------------- | ---------------- |
+| 代表类 | `QListView`/`QTreeView`/`QTableView` | `QListWidget`/`QTreeWidget`/`QTableWidget` |
+| 数据 | 需自建 Model | 自带内部模型 |
+| 数据冗余 | 少 | 相对多 |
+| 效率 | 高(适合大数据) | 适合中小数据 |
+| 操作难度 | 需了解建模 | `addItem`/`setItem` 即可 |
+
+---
+
+## 7. 构建运行步骤
+
+1. **新建项目**:Qt Creator → `New File or Project` → `Application (Qt)` → `Qt Widgets Application`;基类选 `QMainWindow`。
+   - 布局、容器、模型视图类例程(29~46):**不勾选** “Generate form”,用纯代码。
+   - 例 27/28 才需要 `.ui`;本例程组中 27 之前已讲过,29~47 均不勾选。
+2. **替换源码**:把 `mainwindow.h`、`mainwindow.cpp` 替换为本节代码;`main.cpp` 保持默认。
+3. **`QUndoView` 例程**:额外创建 `command.h`、`command.cpp`,并在 `.pro` 的 `SOURCES`/`HEADERS` 中加入相应文件。
+4. **资源文件**(`QToolBox`、`QTabWidget`、`QScrollArea` 例程):按上一篇 1.3 节步骤添加 `.qrc` 与图片。
+5. **检查 `.pro`**:默认内容如下,确保 `QT += widgets` 存在:
+
+   ```qmake
+   QT       += core gui
+   greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+   CONFIG += c++11
+   SOURCES += main.cpp mainwindow.cpp
+   HEADERS += mainwindow.h
+   ```
+
+6. **编译运行**:`Ctrl+R`。
+7. **交叉编译到开发板**:参照《I.MX6U嵌入式Qt开发指南》第十四章配置交叉编译环境,把可执行文件与 Qt 库、字体一起部署到板子运行。
+
+---
+
+## 8. 面试题
+
+### Q1:布局管理器都继承自哪个类?它继承 `QObject` 还是 `QWidget`?
+
+所有基本布局管理类(`QBoxLayout`、`QGridLayout`、`QFormLayout`、`QStackedLayout`)都继承自 `QLayout`,而 `QLayout` 继承自 `QObject`,**不是** `QWidget`。因此布局本身不是部件,但可以通过 `addLayout()` 嵌套组合成复杂布局。
+
+### Q2:`QGridLayout` 与 `QBoxLayout` 设置拉伸因子的方式有何不同?
+
+`QBoxLayout` 用 `setStretch(int index, int stretch)` 为某个子部件设置伸缩因子;`QGridLayout` 因为二维,需要分别用 `setRowStretch(int row, int stretch)` 和 `setColumnStretch(int col, int stretch)` 设置行、列比例系数。
+
+### Q3:`QSpacerItem` 是什么?在布局里还有什么更简便的方法产生间隔?
+
+`QSpacerItem` 继承 `QLayoutItem`,在布局中提供空白间隔,只能在布局里使用,对应 Designer 里的水平/垂直 Spacer。更简便的方法是调用布局的 `addStretch(int)` 直接添加伸缩量,或用 `layout->setSpacing()` 设置统一间距。
+
+### Q4:`QTabWidget` 和 `QStackedWidget` 都能做多页面,二者有什么区别?
+
+`QTabWidget` 自带页签,用户直接点击页签切换,是“页签 + 堆叠”的封装;`QStackedWidget` 只提供堆叠容器,不提供切换入口,通常需要额外搭配 `QListWidget`,用 `currentRowChanged(int)` 连接 `setCurrentIndex(int)` 来实现页面切换(如例 37)。
+
+### Q5:`QListView` 与 `QListWidget` 的本质区别是什么?大项目里怎么选?
+
+`QListView` 基于 Model,需要自己建模(`QStringListModel`、`QStandardItemModel` 等)来保存数据,数据与显示分离、冗余少、效率高;`QListWidget` 基于 Item,本质是“已经封装好模型的 `QListView`”,直接 `addItem` 即可。处理大数据、需要多种视图共享同一数据源时用基于模型的 `QListView`;小数据、追求开发效率时用 `QListWidget`。
+
+---
+
+**内容来源**:《【正点原子】I.MX6U嵌入式Qt开发指南V1.2》第七章 Qt控件 7.5 布局管理、7.6 空间间隔、7.7 容器、7.8 项目视图组(基于模型)、7.9 项目控件组(基于项);配套例程 `Embedded-Qt-Tutorial/Qt/01/29_qboxlayout` ~ `47_qtablewidget`。

+ 1345 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/06-文本绘图与图表.md

@@ -0,0 +1,1345 @@
+---
+title: 文本读写、绘图与图表
+tags: [Qt, QFile, QTextStream, QPainter, QChart, QtCharts, 文本读写, 绘图, 图表, QPen, QBrush, QColor, QIODevice, IMX6ULL, 嵌入式Qt]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第八章 文本读写、第九章 绘图与图表"
+---
+
+# 文本读写、绘图与图表
+
+> 💡 **关联知识**:[[05-Qt应用开发/04-Qt控件详解]]、[[05-Qt应用开发/05-布局容器与模型视图]]、[[05-Qt应用开发/07-多线程网络多媒体与数据库]];延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/10 绘图和绘图设备]]、[[编程学习项目/使用Qt和C++构造UI界面/11 文件系统]]
+
+本篇把 Qt 里三类「偏底层但天天要用」的能力放在一起讲:
+
+1. **文本读写**——用 `QFile` 直接读写文件,或者用 `QTextStream` 以流的方式读写,这是歌词显示、配置文件、日志、字符设备读写的基础;
+2. **绘图**——用 `QPainter` 在窗口(画布)上画点、线、矩形、圆、文字、图片,并做旋转、缩放等坐标变换,这是自定义控件和界面重绘的基础;
+3. **图表**——用 `Qt Charts` 模块(`QChart` / `QChartView` / 各种 `Series`)画出折线图、曲线图、柱状图、饼图,这是数据可视化的基础。
+
+三者都是 Qt **GUI 应用**的能力,和具体硬件无关,因此在 Ubuntu 与 I.MX6ULL 开发板上运行效果一致。本篇示例对应 PDF 第二篇提高篇第八章、第九章,例程编号 `Qt/02/01_qfile` ~ `Qt/02/04_qtchart`。
+
+---
+
+## 1. 文本读写概述
+
+程序运行时经常需要读写文本文件,例如:
+
+- 音乐播放器要读取 Mp3 的歌词文本;
+- 记事本类软件打开 txt、修改后再保存;
+- 保存/恢复用户配置;
+- 在 Linux 下把字符设备当作文件读写(Linux「一切皆文件」),例如第 14.2、14.3 节用 `QFile` 往字符设备写 `'0'` / `'1'` 控制硬件。
+
+Qt 提供了两条主要路线:
+
+| 路线 | 核心类 | 特点 | 适用场景 |
+| ---- | ------ | ---- | -------- |
+| 直接 I/O | `QFile`(继承 `QIODevice`) | 一次读全部 / 按行读 / 直接写字节 | 小文件、整体读写、字符设备 |
+| 流式 I/O | `QTextStream` + `QFile` | 支持 `<<` `>>` 运算符、格式化、编码转换 | 需要按字段/格式化读写文本 |
+
+两者的关系是:`QFile` 负责「打开/关闭/定位/读写字节流」,`QTextStream` 负责「把字节流解释成文本并按格式处理」。`QTextStream` 不能独立存在,它必须绑定到一个 `QIODevice`(如 `QFile`)、`QByteArray` 或 `QString` 上。
+
+```mermaid
+flowchart LR
+    subgraph 应用层
+        A["QTextStream<br/>格式化 / 编码 / 行读取"]
+    end
+    subgraph 设备层
+        B["QFile<br/>打开 / 关闭 / 定位 / 字节读写"]
+    end
+    C["磁盘文件 / 字符设备 / 资源文件"]
+    A -->|"绑定 QIODevice"| B
+    B -->|"read()/write()"| C
+```
+
+> 本章只讲「简单的文本读写」,目的是掌握基本操作。更复杂的文件系统操作(文件信息、目录遍历)由 `QFileInfo`、`QDir` 提供。
+
+---
+
+## 2. QFile 读写文本
+
+### 2.1 QFile 类原理
+
+`QFile` 是一个读写**文本文件、二进制文件和 Qt 资源文件**的 I/O 设备类。它的核心定位是:**把文件抽象成一个可从其中读、可向其写的字节流**。
+
+要点:
+
+- `QFile` 可以单独使用,也可以更方便地和 `QTextStream` 或 `QDataStream` 组合使用;
+- 文件名通常在构造函数里传入,也可以之后用 `setFileName()` 设置;
+- **路径分隔符只能用 `/`**,不支持 `\`。无论在 Windows、Linux 还是 Mac,`QFile` 的文件分隔符都是 `/`。看到 Windows 用 `\`,也不能在 `QFile` 路径里写 `\`;
+- 用 `exists()` 判断文件是否存在,用 `remove()` 删除文件(更高级的文件系统操作由 `QFileInfo`、`QDir` 提供);
+- 用 `open()` 打开,用 `close()` 关闭,用 `flush()` 刷新;
+- 通常用 `QDataStream` 或 `QTextStream` 读写,但也可以调用从 `QIODevice` 继承来的 `read()`、`readLine()`、`readAll()`、`write()`;
+- `QFile` 还继承了 `getChar()`、`putChar()`、`ungetChar()`,一次只处理一个字符;
+- 文件大小由 `size()` 返回;用 `pos()` 获取当前文件位置,用 `seek()` 移动到新位置;到达末尾时 `atEnd()` 返回 `true`。
+
+### 2.2 QIODevice::OpenModeFlag 打开模式
+
+`QFile::open()` 需要一个 `QIODevice::OpenModeFlag` 枚举参数,决定以什么方式打开文件:
+
+| 枚举值 | 含义 | 说明 |
+| ------ | ---- | ---- |
+| `QIODevice::ReadOnly` | 以只读方式打开 | 用于载入文件 |
+| `QIODevice::WriteOnly` | 以只写方式打开 | 用于保存文件 |
+| `QIODevice::ReadWrite` | 以读写方式打开 | 同时可读可写 |
+| `QIODevice::Append` | 以添加模式打开 | 新写入的数据追加到文件尾部 |
+| `QIODevice::Truncate` | 以截取方式打开 | 文件原有内容全部被删除 |
+| `QIODevice::Text` | 以文本方式打开 | 读取时 `\n` 自动翻译为换行符;写入时字符串结束符自动翻译为平台编码,如 Windows 下是 `\r\n` |
+
+这些取值可以**按位或组合**,例如 `QIODevice::ReadOnly | QIODevice::Text` 表示以「只读 + 文本」方式打开。
+
+> ⚠️ **易错点**:`WriteOnly` 单独使用时会**清空原文件内容**(等价于隐式 `Truncate`)。如果想在文件末尾追加而不是覆盖,必须用 `QIODevice::Append`。
+
+### 2.3 QFile 常用 API
+
+| API | 返回类型 | 说明 |
+| --- | -------- | ---- |
+| `QFile(const QString &name)` | — | 构造并指定文件名 |
+| `QFile(QObject *parent)` | — | 构造,父对象为 parent |
+| `setFileName(const QString &name)` | `void` | 设置文件名 |
+| `fileName() const` | `QString` | 获取文件名 |
+| `exists() const` | `bool` | 文件是否存在 |
+| `remove()` | `bool` | 删除文件 |
+| `open(OpenMode flags)` | `bool` | 按模式打开文件 |
+| `close()` | `void` | 关闭文件 |
+| `flush()` | `bool` | 刷新缓冲区 |
+| `readAll()` | `QByteArray` | 读取全部剩余内容 |
+| `read(qint64 maxSize)` | `QByteArray` | 读取最多 maxSize 字节 |
+| `readLine(qint64 maxSize = 0)` | `QByteArray` | 读取一行 |
+| `write(const QByteArray &byteArray)` | `qint64` | 写入字节数组,返回实际写入字节数 |
+| `size() const` | `qint64` | 文件大小 |
+| `pos() const` | `qint64` | 当前读写位置 |
+| `seek(qint64 pos)` | `bool` | 移动到指定位置 |
+| `atEnd() const` | `bool` | 是否到达末尾 |
+
+### 2.4 QFile 操作流程
+
+`QFile` 操作文本文件的流程固定为三步:
+
+```mermaid
+flowchart LR
+    A["创建 QFile 对象"] --> B["设置文件名 / 打开<br/>setFileName + open(模式)"]
+    B --> C["读写<br/>readAll / readLine / write"]
+    C --> D["关闭<br/>close()"]
+```
+
+对应代码骨架:
+
+```cpp
+QFile file("test.txt");                       /* 创建对象并指定文件名 */
+if (!file.open(QIODevice::ReadOnly | QIODevice::Text))  /* 打开 */
+    return;                                   /* 打开失败即返回 */
+QString content = file.readAll();             /* 读全部内容 */
+file.close();                                 /* 关闭 */
+```
+
+### 2.5 完整实例 01_qfile:读写文本(难度:简单)
+
+例程路径 `Qt/02/01_qfile`。功能:点击「打开」按钮弹出系统文件选择框,选中文本文件后把内容读入 `QTextEdit`;修改内容后点击「关闭」按钮,把 `QTextEdit` 的内容写回原文件并清空显示。
+
+#### 2.5.1 mainwindow.h
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QTextEdit>
+#include <QFile>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 用于读取文件后显示 */
+    QTextEdit *textEdit;
+
+    /* QFile 类型对象 */
+    QFile file;
+
+    /* 水平布局 */
+    QHBoxLayout *hBoxLayout;
+
+    /* 垂直布局 */
+    QVBoxLayout *vBoxLayout;
+
+    /* 水平布局 Widget */
+    QWidget *hWidget;
+
+    /* 垂直布局 Widget */
+    QWidget *vWidget;
+
+    /* 打开文件按钮 */
+    QPushButton *openPushButton;
+
+    /* 关闭文件按钮 */
+    QPushButton *closePushButton;
+
+private slots:
+    /* 打开文本文件 */
+    bool openFile();
+
+    /* 关闭文本文件 */
+    void closeFile();
+};
+#endif // MAINWINDOW_H
+```
+
+#### 2.5.2 mainwindow.cpp
+
+```cpp
+#include "mainwindow.h"
+#include <QFileDialog>
+#include <QDebug>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置窗口的位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 布局设置 */
+    textEdit = new QTextEdit();
+    vBoxLayout = new QVBoxLayout();
+    hBoxLayout = new QHBoxLayout();
+    vWidget = new QWidget();
+    hWidget = new QWidget();
+    openPushButton = new QPushButton();
+    closePushButton = new QPushButton();
+
+    /* 设置两个按钮的大小 */
+    openPushButton->setMinimumHeight(50);
+    openPushButton->setMaximumWidth(120);
+    closePushButton->setMinimumHeight(50);
+    closePushButton->setMaximumWidth(120);
+
+    /* 设置两个按钮的文本 */
+    openPushButton->setText("打开");
+    closePushButton->setText("关闭");
+
+    /* 设置关闭按钮为不可用属性,需要打开文件才设置为可用属性 */
+    closePushButton->setEnabled(false);
+
+    /* 水平布局 */
+    hBoxLayout->addWidget(openPushButton);
+    hBoxLayout->addWidget(closePushButton);
+    hWidget->setLayout(hBoxLayout);
+
+    /* 垂直布局 */
+    vBoxLayout->addWidget(textEdit);
+    vBoxLayout->addWidget(hWidget);
+    vWidget->setLayout(vBoxLayout);
+
+    /* 居中 */
+    setCentralWidget(vWidget);
+
+    /* 信号槽连接 */
+    connect(openPushButton, SIGNAL(clicked()),
+            this, SLOT(openFile()));
+    connect(closePushButton, SIGNAL(clicked()),
+            this, SLOT(closeFile()));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+bool MainWindow::openFile()
+{
+    /* 获取文件的路径 */
+    QString fileName = QFileDialog::getOpenFileName(this);
+
+    /* 指向文件 */
+    file.setFileName(fileName);
+
+    /* 判断文件是否存在 */
+    if (!file.exists())
+        return false;
+
+    /* 以读写的方式打开 */
+    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
+        return false;
+
+    /* 读取文本到 textEdit */
+    textEdit->setPlainText(file.readAll());
+
+    /* 设置打开按钮不可用,需要关闭再打开 */
+    openPushButton->setEnabled(false);
+
+    /* 设置关闭按钮为可用属性 */
+    closePushButton->setEnabled(true);
+
+    /* 关闭文件 */
+    file.close();
+
+    return true;
+}
+
+void MainWindow::closeFile()
+{
+    /* 检测打开按钮是否可用,不可用时,说明已经打开了文件 */
+    if (!openPushButton->isEnabled()) {
+        /* 获取 textEdit 的文本内容 */
+        QString str = textEdit->toPlainText();
+
+        /* 以只写的方式打开 */
+        if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
+            return;
+
+        /* 转换为字节数组 */
+        QByteArray strBytes = str.toUtf8();
+
+        /* 写入文件 */
+        file.write(strBytes, strBytes.length());
+
+        /* 清空 textEdit 的显示内容 */
+        textEdit->clear();
+
+        /* 关闭文件 */
+        file.close();
+
+        /* 重新设置打开和关闭按钮的属性 */
+        openPushButton->setEnabled(true);
+        closePushButton->setEnabled(false);
+    }
+}
+```
+
+#### 2.5.3 逐段解释
+
+- **`file` 是成员变量而非局部变量**:`openFile()` 打开、`closeFile()` 写入,两次操作之间要记住「当前操作的是哪个文件」,所以 `QFile file` 声明在头文件里,跨槽函数共享状态。
+- **`QFileDialog::getOpenFileName(this)`**:弹出系统文件选择对话框,返回用户选中文件的绝对路径;用户取消则返回空字符串。此时 `file.setFileName("")`,紧接着的 `exists()` 返回 `false`,函数直接返回,不会崩溃。
+- **`openPushButton` / `closePushButton` 的启用状态当作状态机**:
+  - 打开成功后 `openPushButton->setEnabled(false)`、`closePushButton->setEnabled(true)`;
+  - 关闭写入后反过来。这样避免了重复打开同一个文件;
+- **`closeFile()` 开头的 `if (!openPushButton->isEnabled())`**:只有「已经打开过文件」才允许保存,防止用户没打开就直接点关闭而误写文件。
+- **`str.toUtf8()`**:`QString` 内部是 UTF-16,写入文件前必须转成字节序列。用 `toUtf8()` 保证中文不会乱码。
+- **`file.write(strBytes, strBytes.length())`**:`write()` 写入字节数组,第二个参数是长度;也可以用 `file.write(strBytes)` 让 Qt 自动取长度。
+- **打开模式的选择**:读用 `ReadOnly | Text`,写用 `WriteOnly | Text`。写模式会覆盖原文件内容,这正是「另存/保存」的语义。
+- **`Text` 标志**:读取时把平台换行统一成 `\n`,写入时把 `\n` 转成平台换行(Windows 为 `\r\n`),避免在 Windows 上写出的文件出现「双回车」。
+
+> 说明:实际项目里「打开/关闭文件」一般放在菜单栏用 `QAction` 实现,并加上复制、粘贴、另存为、关闭等动作,完全可以仿照 Windows 记事本写一个 Qt 版记事本。
+
+#### 2.5.4 程序运行效果
+
+1. 点击「打开」,弹出系统文件选择窗口,选择项目路径下的 `测试.txt`;
+2. 文件内容显示在 `QTextEdit` 中,可以直接修改;
+3. 点击「关闭」,修改后的内容写回文件,`QTextEdit` 被清空;
+4. 再次打开该文件,可以看到修改已经保存。
+
+其他常用读写方式(扩展):
+
+```cpp
+/* 按行读取,适合日志、歌词等大文件 */
+QFile file("lyric.txt");
+if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+    while (!file.atEnd()) {
+        QByteArray line = file.readLine();      /* 含行尾换行符 */
+        qDebug() << line.trimmed();             /* trimmed() 去掉首尾空白 */
+    }
+    file.close();
+}
+
+/* 追加写入,不覆盖原内容 */
+if (file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
+    file.write("新的一行\n");
+    file.close();
+}
+
+/* seek / pos 定位读写 */
+file.open(QIODevice::ReadWrite);
+file.seek(10);              /* 移动到第 10 字节处 */
+qint64 p = file.pos();      /* 当前读写位置 */
+file.close();
+```
+
+---
+
+## 3. QTextStream 读写文本
+
+### 3.1 QTextStream 类原理
+
+`QTextStream` 类为读写文本提供了一个方便的接口,常与 `QFile` 结合使用。它可以在 `QIODevice`、`QByteArray` 或 `QString` 上操作。
+
+它的好处:
+
+- 使用**流操作符** `<<`(流插入运算符)和 `>>`(流提取运算符)就能方便地读写单词、行和数字;
+- 支持**字段填充和对齐的格式化选项**,以及数字进制、精度等格式化;
+- 支持**编码设置**,可指定读写时使用的文本编码,避免中文乱码。
+
+`QTextStream` 内部与 C++ 的 `iostream` 思路一致:把一个「设备」包成一个流,然后用运算符逐项读写。
+
+```mermaid
+flowchart LR
+    A["QString / QByteArray / QIODevice"] --> B["QTextStream"]
+    B -->|"operator<<"| C["写入文本"]
+    B -->|"operator>>"| D["读取文本"]
+    B -.-> E["编码 / 填充 / 对齐 / 数字格式"]
+```
+
+### 3.2 编码处理
+
+`QTextStream` 默认按本地编码(`QTextCodec::codecForLocale()`)读写。跨平台或处理中文时,建议显式设置编码:
+
+| API | 说明 |
+| --- | ---- |
+| `setCodec(QTextCodec *codec)` | 设置编解码器(Qt5 常见写法) |
+| `setCodec(const char *codecName)` | 用编码名设置,如 `"UTF-8"` |
+| `setAutoDetectUnicode(bool)` | 自动探测 Unicode |
+| `setGenerateByteOrderMark(bool)` | 写入时是否输出 BOM |
+
+示例:
+
+```cpp
+QFile file("data.txt");
+file.open(QIODevice::WriteOnly | QIODevice::Text);
+QTextStream out(&file);
+out.setCodec("UTF-8");       /* 明确 UTF-8,含中文时避免乱码 */
+out << "温度:25℃" << endl;
+file.close();
+```
+
+> ⚠️ **说明**:`setCodec` 是 Qt5 的常用写法;Qt6 中 `QTextStream` 默认使用 UTF-8,`setCodec` 相关接口有所调整。正点原子教程基于 Qt5,嵌入式开发板上通常也是 Qt5 环境。
+
+### 3.3 格式化选项
+
+`QTextStream` 支持 C++ iostream 风格的格式化:
+
+| API | 说明 |
+| --- | ---- |
+| `setFieldWidth(int)` | 字段总宽度 |
+| `setFieldAlignment(FieldAlignment)` | 对齐方式:`AlignLeft` / `AlignRight` / `AlignCenter` / `AlignAccountingStyle` |
+| `setPadChar(QChar)` | 填充字符,默认空格 |
+| `setRealNumberPrecision(int)` | 浮点数有效位数 |
+| `setRealNumberNotation(RealNumberNotation)` | 浮点表示法:`SmartNotation` / `FixedNotation` / `ScientificNotation` |
+| `setIntegerBase(int)` | 整数进制:2/8/10/16 |
+| `setNumberFlags(NumberFlags)` | 如 `ShowBase`、`UppercaseBase`、`ForceSign` 等 |
+
+带对齐的表格化输出示例:
+
+```cpp
+QTextStream out(stdout);
+out.setFieldWidth(10);
+out.setFieldAlignment(QTextStream::AlignCenter);
+out << "姓名" << "成绩" << endl;
+out.setFieldWidth(0);                    /* 取消字段宽度 */
+out << "张三" << 95 << endl;
+```
+
+`QTextStream` 常用读写 API:
+
+| API | 返回类型 | 说明 |
+| --- | -------- | ---- |
+| `readAll()` | `QString` | 读取全部剩余文本 |
+| `readLine(qint64 maxlen = 0)` | `QString` | 读取一行 |
+| `read(qint64 maxlen)` | `QString` | 读取指定长度 |
+| `atEnd() const` | `bool` | 是否到末尾 |
+| `operator<<(T value)` | `QTextStream &` | 流插入,写入 |
+| `operator>>(T &value)` | `QTextStream &` | 流提取,读取 |
+| `flush()` | `void` | 刷新 |
+| `seek(qint64 pos)` | `bool` | 定位 |
+| `pos() const` | `qint64` | 当前位置 |
+
+### 3.4 完整实例 02_qtextstream:文本流读写(难度:简单)
+
+例程路径 `Qt/02/02_qtextstream`。界面与 `01_qfile` 完全一样,唯一的区别是在 `QFile` 的基础上叠了一层 `QTextStream`。下面给出差异代码,其余代码与 `01_qfile` 相同。
+
+#### 3.4.1 mainwindow.cpp 差异部分
+
+```cpp
+bool MainWindow::openFile()
+{
+    /* 获取文件的路径 */
+    QString fileName = QFileDialog::getOpenFileName(this);
+
+    /* 指向文件 */
+    file.setFileName(fileName);
+
+    /* 判断文件是否存在 */
+    if (!file.exists())
+        return false;
+
+    /* 以读写的方式打开 */
+    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
+        return false;
+
+    /* 使用文本流读取文件 */
+    QTextStream stream(&file);
+
+    /* 读取文本到 textEdit */
+    textEdit->setPlainText(stream.readAll());
+
+    /* 设置打开按钮不可用,需要关闭再打开 */
+    openPushButton->setEnabled(false);
+
+    /* 设置关闭按钮为可用属性 */
+    closePushButton->setEnabled(true);
+
+    /* 关闭文件 */
+    file.close();
+
+    return true;
+}
+
+void MainWindow::closeFile()
+{
+    /* 检测打开按钮是否可用,不可用时,说明已经打开了文件 */
+    if (!openPushButton->isEnabled()) {
+
+        /* 以只写的方式打开 */
+        if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
+            return;
+
+        /* 用文本流读取文件 */
+        QTextStream stream(&file);
+
+        /* 获取 textEdit 的文本内容,转为字符串 */
+        QString str = textEdit->toPlainText();
+
+        /* 使用流提取运算符,写入文本流 */
+        stream << str;
+
+        /* 清空 textEdit 的显示内容 */
+        textEdit->clear();
+
+        /* 关闭文件 */
+        file.close();
+
+        /* 重新设置打开和关闭按钮的属性 */
+        openPushButton->setEnabled(true);
+        closePushButton->setEnabled(false);
+    }
+}
+```
+
+头文件 `mainwindow.h` 与 `01_qfile` 完全一致。
+
+#### 3.4.2 逐段解释
+
+- **`QTextStream stream(&file);`**:构造时把 `QFile` 的地址传进去,表示这个流读写的是 `file` 指向的设备。构造必须在 `file.open()` 成功之后,否则流无法工作。
+- **读**:`stream.readAll()` 返回 `QString`,比起 `QFile::readAll()` 返回 `QByteArray`,它省去了手动转编码的步骤。
+- **写**:`stream << str;` 直接用流插入运算符。
+- **关键差异**:`QFile` 版本写入时要先 `str.toUtf8()` 转字节数组再 `file.write()`;`QTextStream` 版本直接 `stream << str`,由流处理字符编码和换行翻译。
+- **为什么这个例子看不出区别**:因为只做了简单的整体读写。`QTextStream` 真正的优势要等到需要**字段填充/对齐、数字进制、浮点精度、按词读取**时才体现,例如:
+
+```cpp
+QFile file("score.txt");
+file.open(QIODevice::ReadOnly | QIODevice::Text);
+QTextStream in(&file);
+QString name;
+int score;
+while (!in.atEnd()) {
+    in >> name >> score;      /* 按空白分隔依次读取姓名和成绩 */
+    qDebug() << name << score;
+}
+file.close();
+```
+
+#### 3.4.3 程序运行效果
+
+与 8.1.2 小节(即 `01_qfile` 的运行效果)完全一样:点击打开读入文本、修改后点击关闭写回文件。`QFile` 与 `QTextStream` 在简单场景下看起来没有区别。
+
+---
+
+## 4. QPainter 绘图
+
+### 4.1 Qt 绘图体系
+
+Qt 里的所有绘图——包括一个按钮、一个 Label 的显示——都由**绘图系统**执行。绘图系统基于三个类:
+
+| 类 | 角色 | 说明 |
+| -- | ---- | ---- |
+| `QPainter` | 画家 / 绘图操作类 | 提供 `drawLine()`、`drawRect()`、`drawText()` 等绘图接口 |
+| `QPaintDevice` | 画布 / 绘图设备基类 | 提供绘制对象的基类,代表「能画在哪」 |
+| `QPaintEngine` | 绘图引擎 | 提供一个平台上设备的抽象定义,起「抽象平台」的作用 |
+
+它们的关系是:`QPainter` 是直接用来操作绘图的类;`QPaintDevice` 和 `QPaintEngine` 都比 `QPainter` 更底层,一般只需要了解即可。
+
+```mermaid
+flowchart TB
+    A["QPainter<br/>直接绘制图像"] --> B["QPainter 通过 QPaintEngine 绘制"]
+    B --> C["QPaintEngine<br/>提供平台相关的抽象定义"]
+    C --> D["QPaintDevice<br/>提供绘制对象的基类"]
+    D --> E["QWidget / QPixmap / QImage / QLabel ..."]
+```
+
+一般用于显示的类,如 `QWidget`、`QPixmap`、`QImage`、`QLabel` 等可视类控件都可以充当绘图区域的「画布」。从 `QWidget` 继承的类都有 `virtual void paintEvent(QPaintEvent *event);`,这个虚函数在 `qwidget.h` 的 `protected:` 修饰符下。
+
+### 4.2 paintEvent 机制
+
+**什么是绘图事件?** 当界面初始化或需要刷新时才会执行的事件。也就是说:
+
+- 绘图事件在**构造对象实例化时**会执行一次;
+- 需要刷新界面时,调用 `update()` 方法触发 `paintEvent()`。
+
+`paintEvent()` 是父类 `QWidget` 提供给子类的接口,在父类里定义为空,所以它就是专门给子类画图用的。
+
+重写的基本结构:
+
+```cpp
+void Widget::paintEvent(QPaintEvent *)
+{
+    /* 指定画图的对象,this 代表是本 Widget */
+    QPainter painter(this);
+
+    /* 使用 painter 在对象上绘图... */
+}
+```
+
+> ⚠️ **注意**:`paintEvent()` 是 `protected` 的虚函数,重写时应写在子类的 `protected:` 区域。不要在 `paintEvent()` 外部创建 `QPainter` 绘制到窗口,也不要在 `paintEvent()` 里调用 `update()`(会造成无限重绘)。
+
+### 4.3 画笔、画刷与颜色
+
+`QPainter` 需要两个「工具」才能画出好看的东西:
+
+| 工具 | 类 | 作用 |
+| ---- | -- | ---- |
+| 画笔 | `QPen` | 决定线条(轮廓)的颜色、宽度、线型、端点样式、连接样式 |
+| 画刷 | `QBrush` | 决定封闭图形**内部填充**的颜色和样式 |
+| 颜色 | `QColor` | 被 `QPen`、`QBrush` 使用,表示颜色 |
+
+#### 4.3.1 QColor
+
+| 构造 / 方法 | 说明 |
+| ----------- | ---- |
+| `QColor(Qt::GlobalColor)` | 用预定义颜色构造,如 `Qt::red`、`Qt::blue`、`Qt::gray` |
+| `QColor(int r, int g, int b, int a = 255)` | 用 RGB(A) 构造,分量 0~255 |
+| `QColor(const QString &name)` | 用颜色名构造,如 `"#RRGGBB"`、`"red"` |
+| `red()/green()/blue()/alpha()` | 取各分量 |
+| `setRgb(...)` | 设置 RGB(A) |
+
+```cpp
+QColor c1(Qt::red);          /* 预定义 */
+QColor c2(255, 128, 0);      /* 橙色 */
+QColor c3("#00A0FF");        /* 十六进制 */
+QColor c4 = QColor::fromHsv(200, 255, 200);  /* HSV */
+```
+
+#### 4.3.2 QPen
+
+| API | 说明 |
+| --- | ---- |
+| `QPen(const QColor &color)` | 用颜色构造 |
+| `QPen(const QBrush &brush, qreal width)` | 用画刷和线宽构造 |
+| `setColor(QColor)` | 设置颜色 |
+| `setWidth(int)` | 设置线宽(像素) |
+| `setStyle(Qt::PenStyle)` | 线型:`SolidLine`、`DashLine`、`DotLine`、`DashDotLine`、`DashDotDotLine`、`NoPen` |
+| `setCapStyle(Qt::PenCapStyle)` | 端点样式:`FlatCap`、`SquareCap`、`RoundCap` |
+| `setJoinStyle(Qt::PenJoinStyle)` | 连接样式:`MiterJoin`、`BevelJoin`、`RoundJoin` |
+
+```cpp
+QPen pen;
+pen.setColor(Qt::blue);
+pen.setWidth(5);
+pen.setStyle(Qt::DashLine);
+pen.setCapStyle(Qt::RoundCap);
+painter.setPen(pen);
+```
+
+#### 4.3.3 QBrush
+
+| API | 说明 |
+| --- | ---- |
+| `QBrush(Qt::GlobalColor)` | 用预定义颜色构造 |
+| `QBrush(const QColor &color, Qt::BrushStyle)` | 用颜色和样式构造 |
+| `setColor(QColor)` | 设置颜色 |
+| `setStyle(Qt::BrushStyle)` | 填充样式:`SolidPattern`、`NoBrush`、`CrossPattern`、`DiagCrossPattern`、`HorPattern`、`VerPattern`、`Dense1Pattern`~`Dense7Pattern` 等 |
+| `setTexture(QPixmap)` | 用图片纹理填充 |
+| `setTextureImage(QImage)` | 用图片填充 |
+
+```cpp
+QBrush brush;
+brush.setColor(Qt::yellow);
+brush.setStyle(Qt::SolidPattern);
+painter.setBrush(brush);
+```
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书第九章只以旋转 CD 为例讲解 `QPainter`,QPen/QBrush/QColor 属于 Qt 官方文档中 `QPainter` 的标准用法,此处补充以便实际开发使用。
+
+### 4.4 QPainter 常用绘制 API
+
+| API | 说明 |
+| --- | ---- |
+| `drawPoint(const QPointF &position)` | 画点 |
+| `drawLine(const QLineF &line)` | 画直线 |
+| `drawRect(const QRectF &rectangle)` | 画矩形(轮廓 + 填充) |
+| `drawRoundedRect(rect, xRadius, yRadius)` | 画圆角矩形 |
+| `drawEllipse(const QRectF &rectangle)` | 在矩形内画椭圆(内切) |
+| `drawArc(rect, startAngle, spanAngle)` | 画圆弧,角度单位为 1/16 度 |
+| `drawPie(rect, startAngle, spanAngle)` | 画扇形 |
+| `drawPolygon(const QPolygonF &points)` | 画多边形 |
+| `drawPolyline(const QPolygonF &points)` | 画折线 |
+| `drawConvexPolygon(...)` | 画凸多边形 |
+| `drawText(const QPointF &position, const QString &text)` | 在指定位置画文字 |
+| `drawText(const QRectF &rectangle, int flags, const QString &text)` | 在矩形内按对齐标志画文字,如 `Qt::AlignCenter` |
+| `drawImage(const QRectF &target, const QImage &image, const QRectF &source)` | 画图片(指定源与目标矩形) |
+| `drawPixmap(const QRectF &target, const QPixmap &pixmap, const QRectF &source)` | 画 Pixmap |
+| `fillRect(const QRectF &rectangle, const QColor &color)` | 用颜色填充矩形 |
+| `eraseRect(const QRectF &rectangle)` | 擦除矩形区域 |
+| `setPen(const QPen &pen)` / `pen()` | 设置 / 获取画笔 |
+| `setBrush(const QBrush &brush)` / `brush()` | 设置 / 获取画刷 |
+| `setFont(const QFont &font)` | 设置文字字体 |
+| `setRenderHint(RenderHint, bool on = true)` | 设置渲染提示,如抗锯齿 |
+| `translate(qreal dx, qreal dy)` | 平移坐标原点 |
+| `rotate(qreal angle)` | 绕原点旋转(单位:度,顺时针为正) |
+| `scale(qreal sx, qreal sy)` | 缩放坐标系统 |
+| `shear(qreal sh, qreal sv)` | 错切 |
+| `save()` / `restore()` | 保存 / 恢复画笔状态与坐标变换状态 |
+
+### 4.5 坐标变换与状态保存
+
+`QPainter` 的默认坐标系是:**原点 `(0,0)` 在绘图设备左上角,x 向右增大,y 向下增大,单位是像素**。
+
+坐标变换的核心思路:
+
+- `translate(dx, dy)` 移动坐标原点;
+- `rotate(angle)` 绕**当前原点**旋转,默认顺时针;
+- `scale(sx, sy)` 缩放;
+- 多个变换是**叠加**的(矩阵相乘)。
+
+旋转图片时通常这样做:先把原点平移到图片中心,旋转,再把原点平移回去,最后以中心为基准绘制。也可以用 `save()` / `restore()` 成对包裹,避免手工计算反向平移:
+
+```cpp
+painter.save();
+painter.translate(rect.center());   /* 原点到图形中心 */
+painter.rotate(45);                 /* 旋转 45 度 */
+painter.drawImage(QRectF(-rect.width()/2, -rect.height()/2,
+                         rect.width(), rect.height()), image);
+painter.restore();                  /* 恢复坐标系统与画笔 */
+```
+
+### 4.6 抗锯齿
+
+默认绘制的线条和图形边缘有明显锯齿。开启抗锯齿让曲线、斜线更平滑:
+
+| 渲染提示 | 作用 |
+| -------- | ---- |
+| `QPainter::Antialiasing` | 图形边缘抗锯齿 |
+| `QPainter::TextAntialiasing` | 文字抗锯齿 |
+| `QPainter::SmoothPixmapTransform` | 图片平滑缩放 |
+
+```cpp
+painter.setRenderHints(QPainter::Antialiasing
+                       | QPainter::SmoothPixmapTransform);
+```
+
+`QChartView` 上也提供了 `setRenderHint(QPainter::Antialiasing)`。
+
+### 4.7 完整实例 03_qpainter:旋转的 CD(难度:一般)
+
+例程路径 `Qt/02/03_qpainter`。功能:用一张 CD 图片,在 `paintEvent()` 里把 CD 画在窗口中心,并且每 100ms 旋转 1 度,看上去 CD 在旋转。
+
+#### 4.7.1 mainwindow.h
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QPainter>
+#include <QPaintEvent>
+#include <QTimer>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+    /* 重写父类下的 protected 方法 */
+protected:
+    void paintEvent(QPaintEvent *);
+
+private:
+    /* 定时器,用于定时更新界面 */
+    QTimer *timer;
+    /* 角度 */
+    int angle;
+
+private slots:
+    /* 槽函数 */
+    void timerTimeOut();
+};
+#endif // MAINWINDOW_H
+```
+
+`paintEvent()` 是父类 `QWidget` 的 `protected` 虚方法,所以建议重写时也写到子类的 `protected` 修饰符下。
+
+#### 4.7.2 mainwindow.cpp
+
+```cpp
+#include "mainwindow.h"
+#include "QDebug"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗口位置及颜色 */
+    this->setGeometry(0, 0, 800, 480);
+    setPalette(QPalette(Qt::gray));
+    setAutoFillBackground(true);
+
+    /* 定时器实例化 */
+    timer = new QTimer(this);
+
+    /* 默认角度为 0 */
+    angle = 0;
+
+    /* 定时 100ms */
+    timer->start(100);
+
+    /* 信号槽连接 */
+    connect(timer, SIGNAL(timeout()), this, SLOT(timerTimeOut()));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::timerTimeOut()
+{
+    /* 需要更新界面,不设置不更新 */
+    this->update();
+}
+
+void MainWindow::paintEvent(QPaintEvent *)
+{
+    /* 指定父对象,this 指本窗口 */
+    QPainter painter(this);
+
+    /* 设置抗锯齿,流畅转换 */
+    painter.setRenderHints(QPainter::Antialiasing
+                           | QPainter::SmoothPixmapTransform);
+
+    /* 计算旋转角度 */
+    if (angle++ == 360)
+        angle = 0;
+
+    /* QPixmap 类型对象 */
+    QPixmap image;
+
+    /* 加载 */
+    image.load(":/image/cd.png");
+
+    /* QRectF 即继承 QRect(Qt 的矩形类),F 代表精确到浮点类型 */
+    QRectF rect((this->width() - image.width()) / 2,
+                (this->height() - image.height()) / 2,
+                image.width(),
+                image.height());
+
+    /* 默认参考点为左上角原点(0,0),因为旋转需要以图形的中心为参考点,
+     * 我们使用 translate 把参考点设置为 CD 图形的中心点坐标 */
+    painter.translate(0 + rect.x() + rect.width() / 2,
+                      0 + rect.y() + rect.height() / 2);
+
+    /* 旋转角度 */
+    painter.rotate(angle);
+
+    /* 现在参考点为 CD 图形的中心,我们需要把它设置回原点的位置,
+     * 所以需要减去上面加上的数 */
+    painter.translate(0 - (rect.x() + rect.width() / 2),
+                      0 - (rect.y() + rect.height() / 2));
+
+    /* 画图,QPainter 提供了许多 drawX 的方法 */
+    painter.drawImage(rect, image.toImage(), image.rect());
+
+    /* 再画一个矩形 */
+    painter.drawRect(rect.toRect());
+}
+```
+
+#### 4.7.3 逐段解释
+
+- **`setPalette(QPalette(Qt::gray))` + `setAutoFillBackground(true)`**:给窗口设置灰色背景。只有 `autoFillBackground` 为 `true` 时,调色板的背景色才会生效。
+- **`timer->start(100)`**:每 100ms 触发一次 `timeout()`,槽函数里调用 `update()`。`update()` 会安排一次重绘,最终调用 `paintEvent()`。如果这里用 `repaint()` 会立即同步重绘,`update()` 则更高效(可合并多次请求)。
+- **为什么必须调用 `update()`**:`paintEvent()` 在构造函数执行时只会执行一次,不主动刷新就看不到旋转动画。
+- **`angle++ == 360`**:先取当前值再自增;当旧值为 360 时把它重置为 0,形成 0~360 的循环。
+- **`QRectF`**:继承 `QRect`,后缀 `F` 表示坐标精确到浮点(`qreal`)。图片居中靠 `(窗口宽 - 图片宽)/2`、`(窗口高 - 图片高)/2` 计算。
+- **两次 `translate`**:默认参考点是左上角 `(0,0)`,而旋转必须绕图形中心。第一次 `translate` 把原点移到 CD 中心,`rotate(angle)` 绕新原点旋转,第二次 `translate` 把原点平移回去,保证后续 `drawImage` / `drawRect` 使用的 `rect` 坐标仍然以窗口左上角为基准。
+- **`painter.drawImage(rect, image.toImage(), image.rect())`**:目标矩形 `rect`、源图片 `image.toImage()`、源矩形 `image.rect()`(整张图)。
+- **`painter.drawRect(rect.toRect())`**:在图片外框画一个矩形,让旋转效果更明显。
+- **资源文件**:`res.qrc` 把 `image/cd.png` 注册为 `:/image/cd.png`,随可执行文件打包。
+
+#### 4.7.4 程序运行效果
+
+编译运行后,窗口中央的 CD 每 100ms 转 1 度,产生连续的旋转动画;CD 外框还画了一个矩形,使旋转更明显。
+
+用 `paintEvent` 可以实现一些需要绘图的情景,有时比 Qt 动画类更容易实现;结合 Qt 的画笔,也可以设计一个绘图软件,这都得益于 `paintEvent()` 与 `QPainter`。界面重绘时使用 `paintEvent()` 的场合也比较多。
+
+---
+
+## 5. QChart 图表
+
+### 5.1 Qt Charts 模块
+
+在 Qt 5.7 之前,开源社区版本里没有 Qt Charts(自带绘图组件库),这让 QWT、QCustomPlot 等第三方库有了生存空间。**Qt 5.7 之后**,社区版才免费提供 Qt Charts(此前只有商业版才有)。要使用 Qt Charts,Qt 版本必须是 5.7 之后的版本。
+
+Qt Charts 可以方便地绘制常见的**曲线图、折线图、柱状图和饼状图**等,无需了解第三方组件。
+
+在项目里使用 Qt Charts 模块,需要在 `.pro` 文件里添加:
+
+```pro
+QT += charts
+```
+
+除了包含相应头文件,还需要**使用命名空间**。一般在头文件处加:
+
+```cpp
+QT_CHARTS_USE_NAMESPACE
+```
+
+或者在类外写:
+
+```cpp
+using namespace QtCharts;
+```
+
+### 5.2 QChart 继承关系
+
+理解继承关系有助于知道这个类「怎么来的」:
+
+```mermaid
+flowchart BT
+    QObject --> QGraphicsObject
+    QGraphicsItem --> QGraphicsObject
+    QGraphicsItem --> QGraphicsLayoutItem
+    QGraphicsObject --> QGraphicsWidget
+    QGraphicsLayoutItem --> QGraphicsWidget
+    QGraphicsWidget --> QChart
+```
+
+也就是说 `QChart` 本质是一个 `QGraphicsWidget`(它同时继承 `QGraphicsObject` 与 `QGraphicsLayoutItem`),可以像图形项一样被放到 `QGraphicsScene` 里;而 `QChartView` 是承载 `QChart` 的视图控件(继承自 `QGraphicsView`)。在 Qt Creator 里可用 `Ctrl + Shift + T` 查看某个类的继承关系。
+
+### 5.3 Qt Charts 主要类
+
+| 类 | 作用 |
+| -- | ---- |
+| `QChart` | 图表对象,管理系列(Series)、坐标轴(Axis)、图例(Legend)、标题 |
+| `QChartView` | 图表视图,承载并显示 `QChart` |
+| `QAbstractSeries` | 所有系列的基类 |
+| `QLineSeries` | 折线图系列 |
+| `QSplineSeries` | 平滑曲线系列 |
+| `QScatterSeries` | 散点图系列 |
+| `QBarSeries` / `QHorizontalBarSeries` | 柱状图系列 |
+| `QStackedBarSeries` | 堆叠柱状图系列 |
+| `QBarSet` | 柱状图的一组数据 |
+| `QPieSeries` / `QPieSlice` | 饼图系列 / 饼图扇区 |
+| `QAreaSeries` | 面积图系列 |
+| `QValueAxis` | 数值坐标轴 |
+| `QBarCategoryAxis` | 分类坐标轴(柱状图横轴) |
+| `QDateTimeAxis` | 时间坐标轴 |
+| `QLogValueAxis` | 对数坐标轴 |
+| `QLegend` | 图例,用颜色区分不同系列 |
+
+### 5.4 QChart 常用 API
+
+| 类 | API | 说明 |
+| -- | --- | ---- |
+| `QChart` | `addSeries(QAbstractSeries *series)` | 添加系列 |
+| `QChart` | `removeSeries(...)` / `removeAllSeries()` | 移除系列 |
+| `QChart` | `addAxis(QAbstractAxis *axis, Qt::Alignment)` | 添加坐标轴并指定方位 |
+| `QChart` | `setTitle(const QString &)` | 设置标题 |
+| `QChart` | `legend()` | 返回 `QLegend *`,`hide()` / `show()` / `setVisible()` |
+| `QChart` | `setTheme(QChart::ChartTheme)` | 设置主题 |
+| `QChart` | `createDefaultAxes()` | 根据系列自动创建默认坐标轴 |
+| `QChartView` | `setChart(QChart *chart)` | 设置要显示的图表 |
+| `QChartView` | `setRenderHint(QPainter::RenderHint)` | 设置渲染提示,如抗锯齿 |
+| `QAbstractSeries` | `attachAxis(QAbstractAxis *axis)` | 把系列绑定到某条坐标轴 |
+| `QValueAxis` | `setRange(qreal min, qreal max)` | 设置轴范围 |
+| `QValueAxis` | `setTitleText(const QString &)` | 设置轴标题 |
+| `QValueAxis` | `setLabelFormat(const QString &)` | 设置标签格式,如 `"%i"`、`"%g"` |
+| `QValueAxis` | `setTickCount(int count)` | 设置刻度数量 |
+| `QLineSeries` | `append(qreal x, qreal y)` | 追加数据点 |
+| `QLineSeries` | `clear()` | 清空数据点 |
+| `QBarSeries` | `append(QBarSet *set)` | 追加一组数据 |
+| `QBarSet` | `operator<<(qreal value)` | 追加一个数据 |
+| `QBarCategoryAxis` | `append(const QStringList &categories)` | 设置分类标签 |
+| `QPieSeries` | `append(const QString &label, qreal value)` | 追加一个扇区 |
+| `QPieSlice` | `setExploded(bool)` / `setLabelVisible(bool)` | 扇区分离 / 显示标签 |
+
+### 5.5 完整实例 04_qtchart:实时动态曲线(难度:一般)
+
+例程路径 `Qt/02/04_qtchart`。本例基本流程:
+
+1. 创建 `QSplineSeries`(曲线)、`QChart`(图表)、`QChartView`(图表视图);
+2. 创建两条坐标轴 `axisX` 与 `axisY`,添加到 `chart`;
+3. 把 `splineSeries` 与坐标轴联系起来(`attachAxis`);
+4. 把 `chart` 设置到 `chartView`;
+5. 数据由系统产生随机数,用定时器定期更新。
+
+这个例子除了可以绘制静态曲线,也可以绘制动态曲线,稍加改造即可用于实际项目中读取数据绘制实时曲线图。
+
+#### 5.5.1 04_qtchart.pro
+
+```pro
+QT       += core gui charts
+
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+
+CONFIG += c++11
+
+DEFINES += QT_DEPRECATED_WARNINGS
+
+SOURCES += \
+    main.cpp \
+    mainwindow.cpp
+
+HEADERS += \
+    mainwindow.h
+```
+
+关键就是第一行的 `charts` 模块。
+
+#### 5.5.2 mainwindow.h
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+#include <QChartView>
+#include <QSplineSeries>
+#include <QScatterSeries>
+#include <QDebug>
+#include <QValueAxis>
+#include <QTimer>
+#include <QMainWindow>
+
+/* 必须添加命名空间 */
+QT_CHARTS_USE_NAMESPACE
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 接收数据接口 */
+    void receivedData(int);
+
+    /* 数据最大个数 */
+    int maxSize;
+
+    /* x 轴上的最大值 */
+    int maxX;
+
+    /* y 轴上的最大值 */
+    int maxY;
+
+    /* y 轴 */
+    QValueAxis *axisY;
+
+    /* x 轴 */
+    QValueAxis *axisX;
+
+    /* QList int 类型容器 */
+    QList<int> data;
+
+    /* QSplineSeries 对象(曲线)*/
+    QSplineSeries *splineSeries;
+
+    /* QChart 图表 */
+    QChart *chart;
+
+    /* 图表视图 */
+    QChartView *chartView;
+
+    /* 定时器 */
+    QTimer *timer;
+
+private slots:
+    void timerTimeOut();
+};
+#endif // MAINWINDOW_H
+```
+
+#### 5.5.3 mainwindow.cpp
+
+```cpp
+#include "mainwindow.h"
+#include <QDateTime>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置最显示位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+    /* 最大储存 maxSize - 1 个数据 */
+    maxSize = 51;
+    /* x 轴上的最大值 */
+    maxX = 5000;
+    /* y 轴最大值 */
+    maxY = 40;
+
+    /* splineSeries 曲线实例化(折线用 QLineSeries) */
+    splineSeries = new QSplineSeries();
+    /* 图表实例化 */
+    chart = new QChart();
+    /* 图表视图实例化 */
+    chartView = new QChartView();
+
+    /* 坐标轴 */
+    axisY = new QValueAxis();
+    axisX = new QValueAxis();
+    /* 定时器 */
+    timer = new QTimer(this);
+
+    /* legend 译图例类型,以绘图的颜色区分,本例设置为隐藏 */
+    chart->legend()->hide();
+    /* chart 设置标题 */
+    chart->setTitle("实时动态曲线示例");
+    /* 添加一条曲线 splineSeries */
+    chart->addSeries(splineSeries);
+
+    /* 设置显示格式 */
+    axisY->setLabelFormat("%i");
+    /* y 轴标题 */
+    axisY->setTitleText("温度/℃");
+    /* y 轴标题位置(设置坐标轴的方向) */
+    chart->addAxis(axisY, Qt::AlignLeft);
+    /* 设置 y 轴范围 */
+    axisY->setRange(0, maxY);
+    /* 将 splineSeries 附加于 y 轴上 */
+    splineSeries->attachAxis(axisY);
+
+    /* 设置显示格式 */
+    axisX->setLabelFormat("%i");
+    /* x 轴标题 */
+    axisX->setTitleText("时间/ms");
+    /* x 轴标题位置(设置坐标轴的方向) */
+    chart->addAxis(axisX, Qt::AlignBottom);
+    /* 设置 x 轴范围 */
+    axisX->setRange(0, maxX);
+    /* 将 splineSeries 附加于 x 轴上 */
+    splineSeries->attachAxis(axisX);
+
+    /* 将图表的内容设置在图表视图上 */
+    chartView->setChart(chart);
+    /* 设置抗锯齿 */
+    chartView->setRenderHint(QPainter::Antialiasing);
+
+    /* 设置为图表视图为中心部件 */
+    setCentralWidget(chartView);
+
+    /* 定时 200ms */
+    timer->start(200);
+    /* 信号槽连接 */
+    connect(timer, SIGNAL(timeout()), this, SLOT(timerTimeOut()));
+
+    /* 设置随机种子,随机数初始化 */
+    qsrand(time(NULL));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::timerTimeOut()
+{
+    /* 产生随机 0~maxY 之间的数据 */
+    receivedData(qrand() % maxY);
+}
+
+void MainWindow::receivedData(int value)
+{
+    /* 将数据添加到 data 中 */
+    data.append(value);
+
+    /* 当储存数据的个数大于最大值时,把第一个数据删除 */
+    while (data.size() > maxSize) {
+        /* 移除 data 中第一个数据 */
+        data.removeFirst();
+    }
+
+    /* 先清空 */
+    splineSeries->clear();
+
+    /* 计算 x 轴上的点与点之间显示的间距 */
+    int xSpace = maxX / (maxSize - 1);
+
+    /* 添加点,xSpace * i 表示第 i 个点的 x 轴的位置 */
+    for (int i = 0; i < data.size(); ++i) {
+        splineSeries->append(xSpace * i, data.at(i));
+    }
+}
+```
+
+#### 5.5.4 逐段解释
+
+- **对象分工**:`splineSeries` 存数据(点),`chart` 组织图形(系列 + 坐标轴 + 标题 + 图例),`chartView` 负责显示。三者必须按「系列 → chart → chartView」的顺序组装。
+- **`maxSize = 51`**:因为 `xSpace = maxX / (maxSize - 1)`,所以注释说「最大储存 maxSize - 1 个数据」。实际上 `while (data.size() > maxSize)` 允许存 `maxSize` 个,多出的一个会被移除,窗口里稳定显示 51 个点。
+- **`chart->legend()->hide()`**:图例(legend)用颜色区分不同系列。本例只有一条曲线,隐藏图例让画面更干净。
+- **坐标轴绑定三步**:
+  1. `chart->addAxis(axisY, Qt::AlignLeft)`:把轴加到图表并指定方位(左、右、上、下);
+  2. `axisY->setRange(0, maxY)`:设置范围;
+  3. `splineSeries->attachAxis(axisY)`:把系列绑到轴上。**缺少第 3 步曲线不会显示**,这是最常见的错误。
+- **x 轴与 y 轴都要各自 `attachAxis`**:一个系列要显示在二维平面上,必须同时绑定 x 轴和 y 轴。
+- **`chartView->setRenderHint(QPainter::Antialiasing)`**:曲线抗锯齿,看起来更平滑。
+- **`setCentralWidget(chartView)`**:把图表视图设为 `QMainWindow` 的中央部件。
+- **`qsrand(time(NULL))` + `qrand() % maxY`**:设置随机种子并产生 0~maxY-1 的整数,模拟实时采集到的温度数据。
+- **`receivedData()` 的滑动窗口算法**(第 84~105 行,本例核心):
+  1. `data.append(value)` 把新数据加到末尾;
+  2. `while (data.size() > maxSize) data.removeFirst()` 在数据超过上限后删除最老的一个数据;
+  3. `splineSeries->clear()` 清空旧点;
+  4. 按 `xSpace * i` 重新铺 x 坐标,`data.at(i)` 作为 y 值,把窗口内的数据重新画一遍。
+  由于每次 x 位置固定、y 值滚动,曲线看起来就在「向左移动」,实现了数据移动的过程。
+
+> **改造为真实数据源**:把 `qrand() % maxY` 换成串口/CAN/传感器的读数,把 `timerTimeOut()` 换成数据到达信号触发的槽函数,即可用于实际项目的实时曲线。
+
+#### 5.5.5 程序运行效果
+
+运行后,窗口中显示标题为「实时动态曲线示例」的曲线图,横轴为「时间/ms」(0~5000),纵轴为「温度/℃」(0~40)。每 200ms 曲线向左滚动一格并出现一个新的随机值,形成实时动态曲线的效果。
+
+### 5.6 扩展:柱状图与饼图
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书第九章只演示了曲线图,以下为 Qt Charts 官方文档中的标准用法,补充柱状图与饼图以便完整掌握 Qt Charts。
+
+#### 5.6.1 柱状图 QBarSeries
+
+```cpp
+QT_CHARTS_USE_NAMESPACE
+
+/* 一组数据 */
+QBarSet *set = new QBarSet("温度");
+*set << 12 << 22 << 30 << 25 << 18 << 10;
+
+/* 柱状图系列 */
+QBarSeries *series = new QBarSeries();
+series->append(set);
+
+QChart *chart = new QChart();
+chart->addSeries(series);
+chart->setTitle("一天温度变化");
+
+/* 分类横轴 */
+QStringList categories;
+categories << "6时" << "9时" << "12时" << "15时" << "18时" << "21时";
+QBarCategoryAxis *axisX = new QBarCategoryAxis();
+axisX->append(categories);
+chart->addAxis(axisX, Qt::AlignBottom);
+series->attachAxis(axisX);
+
+/* 数值纵轴 */
+QValueAxis *axisY = new QValueAxis();
+axisY->setRange(0, 35);
+axisY->setTitleText("温度/℃");
+chart->addAxis(axisY, Qt::AlignLeft);
+series->attachAxis(axisY);
+
+QChartView *view = new QChartView(chart);
+view->setRenderHint(QPainter::Antialiasing);
+```
+
+#### 5.6.2 饼图 QPieSeries
+
+```cpp
+QPieSeries *series = new QPieSeries();
+series->append("C 语言", 40);
+series->append("C++", 30);
+series->append("Qt", 20);
+series->append("Linux 驱动", 10);
+
+/* 让第二块分离并显示标签 */
+QPieSlice *slice = series->slices().at(1);
+slice->setExploded(true);
+slice->setLabelVisible(true);
+
+QChart *chart = new QChart();
+chart->addSeries(series);
+chart->setTitle("技能占比");
+chart->legend()->setVisible(true);
+chart->legend()->setAlignment(Qt::AlignRight);
+
+QChartView *view = new QChartView(chart);
+view->setRenderHint(QPainter::Antialiasing);
+```
+
+要点:饼图不需要手动创建坐标轴;柱状图的横轴用 `QBarCategoryAxis`(分类轴)而不是 `QValueAxis`。
+
+---
+
+## 6. 构建与运行
+
+### 6.1 所需 Qt 模块
+
+| 例程 | 额外模块 | `.pro` 修改 |
+| ---- | -------- | ----------- |
+| 01_qfile | 无(core gui widgets) | `QT += core gui` |
+| 02_qtextstream | 无 | `QT += core gui` |
+| 03_qpainter | 无(用资源文件) | `QT += core gui`,并加 `RESOURCES += res.qrc` |
+| 04_qtchart | **charts** | `QT += core gui charts` |
+
+通用做法:打开 `.pro` 文件,把第一行改成对应内容;`03_qpainter`、`04_qtchart` 的图片/资源需要随工程一起编译。
+
+### 6.2 Ubuntu 上编译运行
+
+```bash
+# 用 Qt Creator 打开 Qt/02/01_qfile/01_qfile.pro,选择 Desktop Qt 套件
+# 或命令行编译:
+qmake 01_qfile.pro
+make -j4
+./01_qfile
+```
+
+运行 `03_qpainter` 前确认 `res.qrc` 里已包含 `image/cd.png`;运行 `04_qtchart` 前确认 `.pro` 里有 `QT += charts`。
+
+### 6.3 I.MX6ULL 开发板上运行
+
+交叉编译流程与普通 Qt 程序一致(详见交叉编译章节):
+
+1. 用开发板对应的交叉编译 Qt 套件(如 `arm-linux-gnueabihf`)编译;
+2. 把生成的可执行文件拷贝到开发板;
+3. 在开发板 Qt 运行环境(含 Qt Charts 库)下执行。
+
+要点:
+
+- 开发板上的 Qt 必须带 `charts` 模块;若交叉编译的 Qt 未编译 Qt Charts,`04_qtchart` 会链接失败;
+- `paintEvent` 绘图直接使用 framebuffer / EGLFS,无需额外配置;
+- 图片资源建议通过 `res.qrc` 打包进可执行文件,避免开发板上路径问题。
+
+---
+
+## 7. 面试精选
+
+### 7.1 QFile 的 `WriteOnly` 和 `Append` 有什么区别?
+
+`WriteOnly` 单独使用时相当于「只写 + 截断」,会**清空文件原有内容**再从开头写;`Append` 则把新数据**追加到文件末尾**,不破坏原有内容。要做「追加日志」必须用 `QIODevice::WriteOnly | QIODevice::Append`。二者都可以按位或 `QIODevice::Text` 以文本模式打开。
+
+### 7.2 `QFile` 与 `QTextStream` 分别负责什么?为什么常一起用?
+
+`QFile` 继承自 `QIODevice`,负责打开/关闭/定位文件和读写**字节流**,返回 `QByteArray`;`QTextStream` 负责把字节流当成**文本**处理,负责编码转换、按行/按词读取、格式化输出,返回 `QString`。两者一起用时,`QFile` 管设备、`QTextStream` 管文本语义,既能处理编码又能格式化和对齐,因此是处理文本文件的常见组合。
+
+### 7.3 `paintEvent()` 什么时候被调用?为什么旋转动画必须在定时器里调用 `update()`?
+
+`paintEvent()` 是 `QWidget` 的 `protected` 虚函数,在**控件首次显示/实例化**以及**需要重绘时**被调用。程序运行过程中如果数据或角度变了但不通知系统,界面不会自己刷新,所以要在定时器槽函数里调用 `update()`,由 Qt 安排一次重绘事件,从而再次进入 `paintEvent()`。注意不能在 `paintEvent()` 里调用 `update()`,否则会无限重绘。
+
+### 7.4 `QPainter` 旋转图片时为什么要 `translate` 两次?
+
+`QPainter` 默认坐标原点是绘图设备左上角 `(0,0)`,而 `rotate()` 是绕**当前原点**旋转的。若直接旋转,图片会绕左上角转,位置跑偏。所以先把原点平移到图片中心,再 `rotate()`,最后把原点平移回原位,这样旋转就围绕图片中心进行。两次 `translate` 也可以用 `save()` / `restore()` 包裹来简化。
+
+### 7.5 Qt Charts 里一条曲线显示不出来,最可能漏了哪一步?
+
+最可能是漏了 **`series->attachAxis(axis)`**。把坐标轴 `addAxis()` 到 `chart` 只是把轴放进图表,系列还必须通过 `attachAxis()` 分别绑定到 x 轴和 y 轴,且要保证 x、y 两条轴都绑定。其次是:`.pro` 里没加 `QT += charts`、头文件没写 `QT_CHARTS_USE_NAMESPACE`、或者 `setRange()` 的范围不包含数据点导致曲线画在可见区域外。
+
+---
+
+## 延伸阅读
+
+- Qt 绘图与绘图设备深入:[[编程学习项目/使用Qt和C++构造UI界面/10 绘图和绘图设备]]
+- Qt 文件系统:[[编程学习项目/使用Qt和C++构造UI界面/11 文件系统]]
+- 控件基础:[[05-Qt应用开发/04-Qt控件详解]]
+- 布局与模型视图:[[05-Qt应用开发/05-布局容器与模型视图]]
+
+---
+
+**内容来源**:正点原子《I.MX6U嵌入式Qt开发指南V1.2》第八章 文本读写(8.1 QFile 读写文本、8.2 QTextStream 读写文本)、第九章 绘图与图表(9.1 QPainter 绘图、9.2 QChart 图表);例程 `Qt/02/01_qfile`、`02_qtextstream`、`03_qpainter`、`04_qtchart`。

+ 2434 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/07-多线程网络多媒体与数据库.md

@@ -0,0 +1,2434 @@
+---
+title: 多线程、网络、多媒体与数据库
+tags: [Qt, QThread, moveToThread, QMutex, QTcpServer, QTcpSocket, QUdpSocket, 多播, QHostInfo, QNetworkAccessManager, QMediaPlayer, QSound, QAudioRecorder, QtSQL, SQLite, QSqlTableModel, IMX6ULL, 嵌入式Qt]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第十章 多线程、第十一章 网络编程、第十二章 多媒体、第十三章 数据库"
+---
+
+# 多线程、网络、多媒体与数据库
+
+> 💡 **关联知识**:[[05-Qt应用开发/06-文本绘图与图表]]、[[05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战]]、[[05-Qt应用开发/07-多线程网络多媒体与数据库]];延伸阅读:[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]
+
+本篇是 PDF 第二篇提高篇的最后一组内容,把四块「让界面能用起来」的能力串在一起:
+
+1. **多线程**——程序默认只有一个主线程,耗时操作会冻结界面;用 `QThread` 把耗时操作放到子线程;
+2. **网络编程**——用 `QTcpSocket`/`QTcpServer`/`QUdpSocket` 做 TCP/UDP 通信,用 `QHostInfo` 查本机信息,用 `QNetworkAccessManager` 下载文件;
+3. **多媒体**——用 `QSound` 播音效、`QMediaPlayer` 播音乐和视频、`QAudioRecorder` 录音;
+4. **数据库**——用 Qt SQL 模块连接 SQLite,通过 `QSqlQuery` 执行 SQL、`QSqlTableModel`/`QSqlQueryModel` 把表映射到界面。
+
+对应例程:`Qt/02/05_qthread_example1` ~ `Qt/02/18_sqlite_table`,共 14 个。桌面端与 I.MX6ULL 开发板的界面代码完全一致,差异只在交叉编译环境与可用的多媒体插件/解码器。
+
+---
+
+## 1. 多线程
+
+### 1.1 为什么需要多线程
+
+应用程序跑起来后一般情况下只有一个线程,就是**主线程**,线程内的操作都是顺序执行的。假设界面上某个操作比较耗时,例如在嵌入式 CPU 上拍照保存照片(嵌入式 CPU 主频通常远低于 PC,处理速度也慢):
+
+- 界面点击后,耗时操作没完成前,界面会**冻结**、无法响应;
+- 直到操作完成才恢复正常。
+
+用户体验很差。解决办法是**创建一个单独的线程来执行耗时操作**,主线程继续负责界面刷新。
+
+> 官方文档:`QThread` 类提供了一种独立于平台的方法来管理线程。`QThread` 对象在程序中管理一个控制线程,线程在 `run()` 中开始执行。默认情况下 `run()` 通过调用 `exec()` 启动事件循环,并在线程中运行 Qt 事件循环。也可以通过 `QObject::moveToThread()` 把 worker 对象移动到线程中使用。
+
+### 1.2 两种多线程方法
+
+`QThread` 是 Qt 实现多线程的核心类。Qt 有两种多线程写法:
+
+| 方法 | 核心思想 | 说明 |
+| ---- | -------- | ---- |
+| 继承 `QThread`,重写 `run()` | 子类中只有 `run()` 在新线程执行 | Qt 4.8 之前的主流写法 |
+| 继承 `QObject`,`moveToThread()` | 整个 `QObject` 对象移到线程里执行 | Qt 4.8 之后官方推荐,更灵活 |
+
+> Qt 官方从 4.8 之后建议使用第二种方法。两种方法区别不大,但继承 `QObject` 的方法更灵活,所以 Qt 帮助文档里先给继承 `QObject` 的写法,再给继承 `QThread` 的写法。
+
+**关键区别**:继承 `QThread` 时,只有 `run()` 在子线程里,类内其他方法仍在主线程;继承 `QObject` 并用 `moveToThread()` 后,该对象的所有**槽函数**都在子线程里执行,因此可以有 `doWork1()`、`doWork2()` 等多份耗时工作。
+
+```mermaid
+flowchart LR
+    subgraph 继承QThread["方法一:继承 QThread"]
+        A1["主线程<br/>fun1() fun2() ..."] --- A2["子线程<br/>仅 run()"]
+    end
+    subgraph 继承QObject["方法二:继承 QObject + moveToThread"]
+        B1["主线程<br/>fun1() fun2() ..."] -->|"发出信号触发槽"| B2["QThread 线程<br/>doWork1() doWork2() ...<br/>(整个对象移入)"]
+    end
+```
+
+另外 Qt 提供了 `QMutex`、`QMutexLocker`、`QReadLocker`、`QWriteLocker` 等类用于线程之间的同步。
+
+### 1.3 继承 QThread 的线程
+
+例程 `05_qthread_example1`。通过点击按钮开启线程,线程执行完成时发送 `resultReady(const QString &s)` 信号给主线程。
+
+#### 1.3.1 mainwindow.h
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QThread>
+#include <QDebug>
+#include <QPushButton>
+
+/* 使用下面声明的 WorkerThread 线程类 */
+class WorkerThread;
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 在 MainWindow 类里声明对象 */
+    WorkerThread *workerThread;
+
+    /* 声明一个按钮,使用此按钮点击后开启线程 */
+    QPushButton *pushButton;
+
+private slots:
+    /* 槽函数,用于接收线程发送的信号 */
+    void handleResults(const QString &result);
+
+    /* 点击按钮开启线程 */
+    void pushButtonClicked();
+};
+
+/* 新建一个 WorkerThread 类继承于 QThread */
+class WorkerThread : public QThread
+{
+    /* 用到信号槽即需要此宏定义 */
+    Q_OBJECT
+
+public:
+    WorkerThread(QWidget *parent = nullptr) {
+        Q_UNUSED(parent);
+    }
+
+    /* 重写 run 方法,继承 QThread 的类,只有 run 方法是在新的线程里 */
+    void run() override {
+        QString result = "线程开启成功";
+
+        /* 这里写上比较耗时的操作 */
+        // ...
+        // 延时 2s,把延时 2s 当作耗时操作
+        sleep(2);
+
+        /* 发送结果准备好的信号 */
+        emit resultReady(result);
+    }
+
+signals:
+    /* 声明一个信号,译结果准确好的信号 */
+    void resultReady(const QString &s);
+};
+
+#endif // MAINWINDOW_H
+```
+
+#### 1.3.2 mainwindow.cpp
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 对象实例化 */
+    pushButton = new QPushButton(this);
+    workerThread = new WorkerThread(this);
+
+    /* 按钮设置大小与文本 */
+    pushButton->resize(100, 40);
+    pushButton->setText("开启线程");
+
+    /* 信号槽连接 */
+    connect(workerThread, SIGNAL(resultReady(QString)),
+            this, SLOT(handleResults(QString)));
+    connect(pushButton, SIGNAL(clicked()),
+            this, SLOT(pushButtonClicked()));
+}
+
+MainWindow::~MainWindow()
+{
+    /* 进程退出,注意本例 run() 方法没写循环,此方法需要有循环才生效 */
+    workerThread->quit();
+
+    /* 阻塞等待 2000ms 检查一次进程是否已经退出 */
+    if (workerThread->wait(2000)) {
+        qDebug() << "线程已经结束!" << endl;
+    }
+}
+
+void MainWindow::handleResults(const QString &result)
+{
+    /* 打印出线程发送过来的结果 */
+    qDebug() << result << endl;
+}
+
+void MainWindow::pushButtonClicked()
+{
+    /* 检查线程是否在运行,如果没有则开始运行 */
+    if (!workerThread->isRunning())
+        workerThread->start();
+}
+```
+
+#### 1.3.3 逐段解释
+
+- **`QThread` 对象管理线程**:Qt 使用 C++ 基本是对象编程,线程也不例外,用对象来管理线程。
+- **`run() override`**:把耗时操作写在这里。本例相当于一个继承 `QThread` 线程的模板。注意只有 `run()` 在新线程里。
+- **`sleep(2)`**:`QThread` 提供的静态保护方法,此处用 2s 延时模拟耗时操作。
+- **`emit resultReady(result)`**:在子线程内发射信号,主线程接收后执行 `handleResults()`。信号槽是线程安全的跨线程通信方式。
+- **`workerThread->start()`**:`start()` 会调用重写的 `run()`,启动新线程。用 `isRunning()` 判断,避免 2s 内重复点击重复启动。
+- **析构里 `quit()` + `wait(2000)`**:`quit()` 让线程事件循环退出;`wait()` 阻塞等待线程结束,返回 `true` 表示正常结束。本例 `run()` 没有循环,所以 `quit()` 对其影响有限,主要是演示退出流程。
+
+#### 1.3.4 运行效果
+
+点击「开启线程」按钮后延时 2s,应用程序输出窗口打印「线程开启成功」。2s 内多次点击不会重复开启线程(线程还在运行),且点击按钮没有卡顿——延时操作在子线程里,按钮在主线程里。
+
+关闭程序后,子线程在主线程析构函数里退出,等待 2s 后打印「线程已经结束」。
+
+### 1.4 继承 QObject 的线程(moveToThread)
+
+例程 `06_qthread_example2`。点击按钮开启线程,另一个按钮点击关闭线程,并通过加锁安全地终止一个耗时循环。
+
+**为什么需要加锁来终止线程?** `quit()` 和 `exit()` 都不会中途终止线程;要马上终止可以用 `terminate()`,但该函数非常不安全,Qt 官方文档不推荐。因此通常加一个 `bool` 变量,由主线程修改它来通知子线程退出循环。由于这个 bool 变量会被两个线程访问,可能引起访问冲突,所以需要加锁。加锁会消耗一定性能、增加耗时,但保证了安全。
+
+```mermaid
+flowchart TB
+    M["主线程 MainWindow"] -->|"startWork(QString) 信号"| W
+    subgraph T["workerThread 线程"]
+        W["worker 对象(继承 QObject,已 moveToThread)"]
+        W --> D1["耗时的 doWork1()"]
+        W --> D2["doWork2() ..."]
+    end
+    W -->|"resultReady(QString) 信号"| M
+    M -->|"worker->stopWork() 直接调用,加锁修改标志位"| W
+```
+
+#### 1.4.1 mainwindow.h
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QThread>
+#include <QDebug>
+#include <QPushButton>
+#include <QMutexLocker>
+#include <QMutex>
+
+/* 工人类 */
+class Worker;
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 开始线程按钮 */
+    QPushButton *pushButton1;
+
+    /* 打断线程按钮 */
+    QPushButton *pushButton2;
+
+    /* 全局线程 */
+    QThread workerThread;
+
+    /* 工人类 */
+    Worker *worker;
+
+private slots:
+    /* 按钮1 点击开启线程 */
+    void pushButton1Clicked();
+
+    /* 按钮2 点击打断线程 */
+    void pushButton2Clicked();
+
+    /* 用于接收工人是否在工作的信号 */
+    void handleResults(const QString &);
+
+signals:
+    /* 工人开始工作(做些耗时的操作 ) */
+    void startWork(const QString &);
+};
+
+/* Worker 类,这个类声明了 doWork1 函数,将整个 Worker 类移至线程 workerThread */
+class Worker : public QObject
+{
+    Q_OBJECT
+
+private:
+    /* 互斥锁 */
+    QMutex lock;
+
+    /* 标志位 */
+    bool isCanRun;
+
+public slots:
+    /* 耗时的工作都放在槽函数下,工人可以有多份不同的工作,但是每次只能去做一份 */
+    void doWork1(const QString &parameter) {
+
+        /* 标志位为真 */
+        isCanRun = true;
+
+        /* 死循环 */
+        while (1) {
+            /* 此 {} 作用是 QMutexLocker 与 lock 的作用范围,获取锁后,
+             * 运行完成后即解锁 */
+            {
+                QMutexLocker locker(&lock);
+                /* 如果标志位不为真 */
+                if (!isCanRun) {
+                    /* 跳出循环 */
+                    break;
+                }
+            }
+            /* 使用 QThread 里的延时函数,当作一个普通延时 */
+            QThread::sleep(2);
+
+            emit resultReady(parameter + "doWork1 函数");
+        }
+        /* doWork1 运行完成,发送信号 */
+        emit resultReady("打断doWork1 函数");
+    }
+
+    // void doWork2();...
+
+public:
+    /* 打断线程(注意此方法不能放在槽函数下) */
+    void stopWork() {
+        qDebug() << "打断线程" << endl;
+
+        /* 获取锁后,运行完成后即解锁 */
+        QMutexLocker locker(&lock);
+        isCanRun = false;
+    }
+
+signals:
+    /* 工人工作函数状态的信号 */
+    void resultReady(const QString &result);
+};
+#endif // MAINWINDOW_H
+```
+
+#### 1.4.2 mainwindow.cpp
+
+```cpp
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置显示位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+    pushButton1 = new QPushButton(this);
+    pushButton2 = new QPushButton(this);
+
+    /* 设置按钮的位置大小 */
+    pushButton1->setGeometry(300, 200, 80, 40);
+    pushButton2->setGeometry(400, 200, 80, 40);
+
+    /* 设置两个按钮的文本 */
+    pushButton1->setText("开启线程");
+    pushButton2->setText("打断线程");
+
+    /* 工人类实例化 */
+    worker = new Worker;
+
+    /* 将 worker 类移至线程 workerThread */
+    worker->moveToThread(&workerThread);
+
+    /* 信号槽连接 */
+
+    /* 线程完成销毁对象 */
+    connect(&workerThread, SIGNAL(finished()),
+            worker, SLOT(deleteLater()));
+    connect(&workerThread, SIGNAL(finished()),
+            &workerThread, SLOT(deleteLater()));
+
+    /* 发送开始工作的信号,开始工作 */
+    connect(this, SIGNAL(startWork(QString)),
+            worker, SLOT(doWork1(QString)));
+
+    /* 接收到 worker 发送过来的信号 */
+    connect(worker, SIGNAL(resultReady(QString)),
+            this, SLOT(handleResults(QString)));
+
+    /* 点击按钮开始线程 */
+    connect(pushButton1, SIGNAL(clicked()),
+            this, SLOT(pushButton1Clicked()));
+
+    /* 点击按钮打断线程 */
+    connect(pushButton2, SIGNAL(clicked()),
+            this, SLOT(pushButton2Clicked()));
+}
+
+MainWindow::~MainWindow()
+{
+    /* 打断线程再退出 */
+    worker->stopWork();
+    workerThread.quit();
+
+    /* 阻塞线程 2000ms,判断线程是否结束 */
+    if (workerThread.wait(2000)) {
+        qDebug() << "线程结束" << endl;
+    }
+}
+
+void MainWindow::pushButton1Clicked()
+{
+    /* 字符串常量 */
+    const QString str = "正在运行";
+
+    /* 判断线程是否在运行 */
+    if (!workerThread.isRunning()) {
+        /* 开启线程 */
+        workerThread.start();
+    }
+
+    /* 发送正在运行的信号,线程收到信号后执行后返回线程耗时函数 + 此字符串 */
+    emit this->startWork(str);
+}
+
+void MainWindow::pushButton2Clicked()
+{
+    /* 如果线程在运行 */
+    if (workerThread.isRunning()) {
+
+        /* 停止耗时工作,跳出耗时工作的循环 */
+        worker->stopWork();
+    }
+}
+
+void MainWindow::handleResults(const QString &results)
+{
+    /* 打印线程的状态 */
+    qDebug() << "线程的状态:" << results << endl;
+}
+```
+
+#### 1.4.3 逐段解释
+
+- **工人类实例化不能指定父对象**:继承 `QObject` 的多线程类不能用 `new Worker(this)`,因为它的「父」是主线程对象,指定父对象会破坏 `moveToThread` 的前提(不能把有父对象的对象移到别的线程)。
+- **`worker->moveToThread(&workerThread)`**:把 worker 对象整体移到 `workerThread` 线程里。此后 worker 的槽函数都在该线程执行。
+- **`connect(&workerThread, SIGNAL(finished()), worker, SLOT(deleteLater()))`**:线程结束后用 `deleteLater()` 销毁 worker 对象与 workerThread 对象分配的内存。`deleteLater()` 会确认消息循环中没有这两个对象后再销毁,避免内存泄漏。
+- **`connect(this, SIGNAL(startWork(QString)), worker, SLOT(doWork1(QString)))`**:主线程发射 `startWork`,worker 的 `doWork1()` 在子线程执行。这是跨线程调用槽函数的标准方式。
+- **`doWork1()` 里的 while 循环 + 锁**:
+  - 进入循环后用 `QMutexLocker locker(&lock)` 加锁,判断 `isCanRun`,为假就 `break`;
+  - `{}` 限定锁的作用范围,离开代码块自动解锁,`QThread::sleep(2)` 不会持锁;
+  - 每 2s 发射一次 `resultReady`。
+- **`stopWork()` 不能放在槽函数下**:它是被主线程**直接调用**的(不是信号触发),所以是普通 `public` 成员函数,不是槽。里面同样用 `QMutexLocker` 加锁后把 `isCanRun = false`,通知循环跳出。
+- **`QMutexLocker` 的价值**:在构造时加锁、析构时解锁,即使中途 return/异常也能保证解锁,避免忘记 `unlock()` 造成死锁。
+- **`doWork1()` 结束不等于线程结束**:因为 worker 被移到了线程里,线程会一直存在,直到对象被销毁,或调用 `quit()`/`exit()` 退出线程事件循环。
+- **析构顺序**:先 `stopWork()` 让循环跳出,再 `workerThread.quit()`,最后 `wait(2000)` 确认退出。
+
+#### 1.4.4 运行效果
+
+点击「开启线程」后,输出窗口每隔 2 秒打印「正在运行 doWork1 函数」;点击「打断线程」后打印「打断 doWork1 函数」,循环结束。再次点击「开启线程」可再次运行 `doWork1()`。
+
+### 1.5 线程同步类
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书只提到 Qt 提供 `QMutex`、`QMutexLocker`、`QReadLocker`、`QWriteLocker` 等类用于线程同步,此处的对照表为 Qt 官方文档标准内容,补充以便实际开发查阅。
+
+| 类 | 作用 | 典型用法 |
+| -- | ---- | -------- |
+| `QMutex` | 互斥锁,保护临界区 | `lock.lock(); ...; lock.unlock();` |
+| `QMutexLocker` | RAII 方式安全使用 `QMutex` | `QMutexLocker locker(&lock);` 作用域结束自动解锁 |
+| `QReadLocker` / `QWriteLocker` | 配合 `QReadWriteLock` 的读/写锁 | 读多写少场景提升并发 |
+| `QSemaphore` | 信号量,控制同时访问资源的线程数 | 生产者/消费者缓冲 |
+| `QWaitCondition` | 条件变量,等待某个条件成立 | 线程间通知 |
+
+> ⚠️ **易错点**:跨线程访问共享变量(如本例的 `isCanRun`)必须加锁,否则可能出现读到旧值、编译器重排等问题。锁的粒度要尽量小,不要在处理耗时操作时持锁。
+
+### 1.6 跨线程信号与槽
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。原书示例通过信号把子线程结果传回主线程,但未展开连接类型,以下为 Qt 官方文档中信号槽跨线程的标准规则。
+
+Qt 的信号槽天生支持跨线程:
+
+- 默认连接方式 `Qt::AutoConnection`:若信号发射者与接收者不在同一线程,自动使用 `Qt::QueuedConnection`(把调用排入接收者线程的事件循环,异步执行);在同一线程则用 `Qt::DirectConnection`(直接调用)。
+- `Qt::QueuedConnection` 要求参数类型可被元对象系统复制,因此自定义类型需要 `qRegisterMetaType()` 注册。
+- **子线程不能直接操作界面控件**:所有界面更新都应通过信号回到主线程执行,否则行为未定义。这也是本例中 worker 只发 `resultReady` 信号、由主线程打印的原因。
+
+---
+
+## 2. Qt 网络编程
+
+Qt 网络模块提供了编写 TCP/IP 客户端和服务器的类:
+
+- **低级类**:`QTcpSocket`、`QTcpServer`、`QUdpSocket`,代表低层网络概念;
+- **高级类**:`QNetworkRequest`、`QNetworkReply`、`QNetworkAccessManager`,用通用协议执行网络操作;
+- 还提供 `QNetworkConfiguration`、`QNetworkConfigurationManager`、`QNetworkSession` 等类实现承载管理。
+
+在项目中使用 Qt 网络模块,需要在 `.pro` 文件里增加:
+
+```pro
+QT += network
+```
+
+### 2.1 获取本机网络信息
+
+**为什么先获取本机网络信息?** 建立网络通信前至少要获取对方的 IP 地址。网络应用中经常需要本机的主机名、IP 地址、MAC 地址等信息,Qt 提供了 `QHostInfo` 和 `QNetworkInterface` 类用于此类信息查询。
+
+例程 `07_networkhostinfo`。点击「获取本机信息」按钮后延时 1s 刷新并显示网络接口信息,点击「清空文本信息」清空显示。
+
+#### 2.1.1 核心代码:getHostInfo()
+
+```cpp
+#include <QNetworkInterface>
+#include <QHostInfo>
+
+QString MainWindow::getHostInfo()
+{
+    /* 通过 QHostInfo 的 localHostName 函数获取主机名称 */
+    QString str = "主机名称:" + QHostInfo::localHostName() + "\n";
+
+    /* 获取所有的网络接口,
+     * QNetworkInterface 类提供主机的 IP 地址和网络接口的列表 */
+    QList<QNetworkInterface> list
+            = QNetworkInterface::allInterfaces();
+
+    /* 遍历 list */
+    foreach (QNetworkInterface interface, list) {
+        str += "网卡设备:" + interface.name() + "\n";
+        str += "MAC 地址:" + interface.hardwareAddress() + "\n";
+
+        /* QNetworkAddressEntry 类存储 IP 地址子网掩码和广播地址 */
+        QList<QNetworkAddressEntry> entryList
+                = interface.addressEntries();
+
+        /* 遍历 entryList */
+        foreach (QNetworkAddressEntry entry, entryList) {
+            /* 过滤 IPv6 地址,只留下 IPv4 */
+            if (entry.ip().protocol() ==
+                    QAbstractSocket::IPv4Protocol) {
+                str += "IP 地址:" + entry.ip().toString() + "\n";
+                str += "子网掩码:" + entry.netmask().toString() + "\n";
+                str += "广播地址:" + entry.broadcast().toString() + "\n\n";
+            }
+        }
+    }
+
+    /* 返回网络信息 */
+    return str;
+}
+```
+
+槽函数与定时器:
+
+```cpp
+void MainWindow::timerStart()
+{
+    /* 清空文本 */
+    textBrowser->clear();
+
+    /* 定时 1s */
+    timer->start(1000);
+}
+
+void MainWindow::timerTimeOut()
+{
+    /* 显示本机信息 */
+    showHostInfo();
+    /* 停止定时器 */
+    timer->stop();
+}
+
+void MainWindow::showHostInfo()
+{
+    /* 获取本机信息后显示到 textBrowser */
+    textBrowser->insertPlainText(getHostInfo());
+}
+
+void MainWindow::clearHostInfo()
+{
+    if (!textBrowser->toPlainText().isEmpty())
+        textBrowser->clear();
+}
+```
+
+#### 2.1.2 逐段解释
+
+- **`QHostInfo::localHostName()`**:获取主机名称。
+- **`QNetworkInterface::allInterfaces()`**:获取所有网络接口列表。直接打印 `list` 可以看到所有网络信息,但要从接口里提取细节需要 `QNetworkAddressEntry`。
+- **`interface.addressEntries()`**:从接口获取所有地址条目。
+- **`entry.ip().protocol() == QAbstractSocket::IPv4Protocol`**:一个接口下可能有 IPv4 和 IPv6 两个地址,用协议类型过滤只保留 IPv4。
+
+#### 2.1.3 运行效果
+
+点击「获取本机信息」,文本浏览框打印主机名、网卡名、MAC 地址、IPv4 地址、子网掩码、广播地址。`lo` 是本地回环网卡,`ens33` 是 VMware 虚拟出来的网卡(Ubuntu 虚拟机环境)。点击「清空文本信息」清空内容。
+
+### 2.2 TCP 通信
+
+#### 2.2.1 TCP 简介
+
+TCP(Transmission Control Protocol,传输控制协议)是一种**面向连接的、可靠的、基于字节流的传输层通信协议**。TCP 通信必须先建立连接,通信端分为客户端和服务端:
+
+- 服务端通过监听某个端口来等待客户端连接,有连接到来则建立新的 socket 连接;
+- 客户端通过 IP 和 port 连接服务端,连接成功后即可收发数据。
+
+> **注意**:在 Qt 中,Qt 把 socket 当成输入输出流来对待,数据收发通过 `read()` 和 `write()` 进行,需要与常见 C socket 的 `send()` / `recv()` 区分。
+
+```mermaid
+sequenceDiagram
+    participant C as TCP 客户端 (QTcpSocket)
+    participant S as TCP 服务端 (QTcpServer + QTcpSocket)
+    S->>S: listen(IP, port)
+    C->>S: connectToHost(IP, port)
+    S-->>S: 发射 newConnection()
+    S->>S: nextPendingConnection() 获取通信 socket
+    S-->>C: 连接建立
+    C->>S: write(data)
+    S-->>C: readyRead() → readAll()
+    S->>C: write(data)
+    C-->>S: readyRead() → readAll()
+    C->>S: disconnectFromHost()
+```
+
+#### 2.2.2 TCP 服务端(08_tcpserver)
+
+流程:获取本地 IP → 创建 `tcpServer` 与 `tcpSocket` → 点击监听即绑定本地 IP 和端口等待连接 → 客户端连上后收发消息。
+
+**核心代码:**
+
+```cpp
+/* 新的客户端连接 */
+void MainWindow::clientConnected()
+{
+    /* 获取客户端的套接字 */
+    tcpSocket = tcpServer->nextPendingConnection();
+    /* 客户端的 ip 信息 */
+    QString ip = tcpSocket->peerAddress().toString();
+    /* 客户端的端口信息 */
+    quint16 port = tcpSocket->peerPort();
+    /* 在文本浏览框里显示出客户端的连接信息 */
+    textBrowser->append("客户端已连接");
+    textBrowser->append("客户端ip 地址:" + ip);
+    textBrowser->append("客户端端口:" + QString::number(port));
+
+    connect(tcpSocket, SIGNAL(readyRead()),
+            this, SLOT(receiveMessages()));
+    connect(tcpSocket,
+            SIGNAL(stateChanged(QAbstractSocket::SocketState)),
+            this,
+            SLOT(socketStateChange(QAbstractSocket::SocketState)));
+}
+
+/* 获取本地 IP */
+void MainWindow::getLocalHostIP()
+{
+    QList<QNetworkInterface> list
+            = QNetworkInterface::allInterfaces();
+    foreach (QNetworkInterface interface, list) {
+        QList<QNetworkAddressEntry> entryList
+                = interface.addressEntries();
+        foreach (QNetworkAddressEntry entry, entryList) {
+            if (entry.ip().protocol() ==
+                    QAbstractSocket::IPv4Protocol) {
+                comboBox->addItem(entry.ip().toString());
+                /* 添加到 IP 列表中 */
+                IPlist << entry.ip();
+            }
+        }
+    }
+}
+
+/* 开始监听 */
+void MainWindow::startListen()
+{
+    /* 需要判断当前主机是否有 IP 项 */
+    if (comboBox->currentIndex() != -1) {
+        qDebug() << "start listen" << endl;
+        tcpServer->listen(IPlist[comboBox->currentIndex()],
+                spinBox->value());
+
+        /* 设置按钮与下拉列表框的状态 */
+        pushButton[0]->setEnabled(false);
+        pushButton[1]->setEnabled(true);
+        comboBox->setEnabled(false);
+        spinBox->setEnabled(false);
+
+        textBrowser->append("服务器IP 地址:" + comboBox->currentText());
+        textBrowser->append("正在监听端口:" + spinBox->text());
+    }
+}
+
+/* 停止监听 */
+void MainWindow::stopListen()
+{
+    qDebug() << "stop listen" << endl;
+    /* 停止监听 */
+    tcpServer->close();
+
+    /* 如果是连接上了也应该断开,如果不断开客户端还能继续发送信息,
+     * 因为 socket 未断开,还在监听上一次端口 */
+    if (tcpSocket->state() == tcpSocket->ConnectedState)
+        tcpSocket->disconnectFromHost();
+
+    /* 设置按钮与下拉列表框的状态 */
+    pushButton[1]->setEnabled(false);
+    pushButton[0]->setEnabled(true);
+    comboBox->setEnabled(true);
+    spinBox->setEnabled(true);
+
+    textBrowser->append("已停止监听端口:" + spinBox->text());
+}
+
+/* 服务端接收消息 */
+void MainWindow::receiveMessages()
+{
+    /* 读取接收到的消息 */
+    QString messages = "客户端:" + tcpSocket->readAll();
+    textBrowser->append(messages);
+}
+
+/* 服务端发送消息 */
+void MainWindow::sendMessages()
+{
+    if (NULL == tcpSocket)
+        return;
+
+    /* 如果已经连接 */
+    if (tcpSocket->state() == tcpSocket->ConnectedState) {
+        /* 发送消息 */
+        tcpSocket->write(lineEdit->text().toUtf8().data());
+        /* 在服务端插入发送的消息 */
+        textBrowser->append("服务端:" + lineEdit->text());
+    }
+}
+```
+
+在构造函数里需要连接 `newConnection` 信号:
+
+```cpp
+tcpServer = new QTcpServer(this);
+tcpSocket = new QTcpSocket(this);
+/* ... 界面代码 ... */
+connect(tcpServer, SIGNAL(newConnection()),
+        this, SLOT(clientConnected()));
+```
+
+**逐段解释:**
+
+- **`tcpServer->listen(QHostAddress, quint16)`**:绑定 IP 与端口并开始监听。端口范围设为 10000~99999,避免与系统已用端口冲突。
+- **`newConnection()` 信号**:有客户端连接时发出,槽函数里用 `nextPendingConnection()` 取出该连接的 `QTcpSocket`。
+- **`peerAddress()` / `peerPort()`**:获取对端(客户端)的 IP 与端口。
+- **`readyRead()` 信号**:有新数据可读时发出,槽函数里 `readAll()` 读出全部内容。
+- **`stateChanged(QAbstractSocket::SocketState)`**:socket 状态变化(未连接、已连接、连接中、主机查找、关闭中、监听中、已绑定)时发出,用于界面提示。
+- **`tcpSocket->write(...)`**:发送数据,参数为字节数组。这里用 `.toUtf8().data()` 把 `QString` 转成 UTF-8 字节。
+- **`stopListen()` 里先 `tcpServer->close()` 再判断断连**:只关服务器不断开已建立的 socket 的话,客户端仍能继续发数据。
+
+#### 2.2.3 TCP 客户端(09_tcpclient)
+
+流程:获取本地 IP → 创建 `tcpSocket` → 用 `connectToHost()` 连接服务端 IP 和端口 → 收发数据。
+
+**核心代码:**
+
+```cpp
+void MainWindow::toConnect()
+{
+    /* 如果连接状态还没有连接 */
+    if (tcpSocket->state() != tcpSocket->ConnectedState) {
+        /* 指定 IP 地址和端口连接 */
+        tcpSocket->connectToHost(IPlist[comboBox->currentIndex()],
+                spinBox->value());
+    }
+}
+
+void MainWindow::toDisConnect()
+{
+    /* 断开连接 */
+    tcpSocket->disconnectFromHost();
+    /* 关闭 socket */
+    tcpSocket->close();
+}
+
+void MainWindow::connected()
+{
+    /* 显示已经连接 */
+    textBrowser->append("已经连上服务端");
+
+    /* 设置按钮与下拉列表框的状态 */
+    pushButton[0]->setEnabled(false);
+    pushButton[1]->setEnabled(true);
+    comboBox->setEnabled(false);
+    spinBox->setEnabled(false);
+}
+
+void MainWindow::disconnected()
+{
+    /* 显示已经断开连接 */
+    textBrowser->append("已经断开服务端");
+
+    pushButton[1]->setEnabled(false);
+    pushButton[0]->setEnabled(true);
+    comboBox->setEnabled(true);
+    spinBox->setEnabled(true);
+}
+
+/* 客户端接收消息 */
+void MainWindow::receiveMessages()
+{
+    QString messages = tcpSocket->readAll();
+    textBrowser->append("服务端:" + messages);
+}
+
+/* 客户端发送消息 */
+void MainWindow::sendMessages()
+{
+    if (NULL == tcpSocket)
+        return;
+
+    if (tcpSocket->state() == tcpSocket->ConnectedState) {
+        textBrowser->append("客户端:" + lineEdit->text());
+        tcpSocket->write(lineEdit->text().toUtf8().data());
+    }
+}
+```
+
+构造函数里的信号槽:
+
+```cpp
+connect(tcpSocket, SIGNAL(connected()),    this, SLOT(connected()));
+connect(tcpSocket, SIGNAL(disconnected()), this, SLOT(disconnected()));
+connect(tcpSocket, SIGNAL(readyRead()),    this, SLOT(receiveMessages()));
+connect(tcpSocket,
+        SIGNAL(stateChanged(QAbstractSocket::SocketState)),
+        this,
+        SLOT(socketStateChange(QAbstractSocket::SocketState)));
+```
+
+**逐段解释:**
+
+- **`connectToHost(IP, port)`**:客户端主动连接服务端。连接是异步的,成功后会发出 `connected()` 信号。
+- **`connected()` / `disconnected()` 信号**:分别在连接建立、断开时发出,用于更新界面按钮状态与提示。
+- **发送与接收都用同一个 `tcpSocket`**:Qt 把 socket 当流处理,`write()` 发送、`readAll()` 接收。
+- **`disconnectFromHost()` + `close()`**:主动断开并释放 socket。
+
+#### 2.2.4 运行效果
+
+两个程序可运行在同一台机器(服务端监听本机 IP,客户端连接同一 IP)或分别运行在开发板与 Ubuntu 上。服务端先点「开始监听」,客户端点「连接服务端」,双方即可互发消息;服务端会显示客户端 IP 与端口,客户端会显示连接状态。停止监听或断开连接时,双方界面上的按钮状态自动切换。
+
+### 2.3 UDP 通信
+
+#### 2.3.1 UDP 简介
+
+UDP 是**无连接**的传输层协议,不需要事先建立连接,直接把数据报(datagram)发出去,速度快但不保证可靠。UDP 支持三种发送方式:
+
+| 方式 | 目标地址 | 说明 |
+| ---- | -------- | ---- |
+| 单播 | 某个具体 IP | 一对一,点对点发送 |
+| 广播 | `QHostAddress::Broadcast`(255.255.255.255) | 同一网段内监听目标端口的程序都会收到 |
+| 组播(多播) | D 类地址,如 `239.255.255.1` | 加入同一组播组的主机都能收到 |
+
+```mermaid
+flowchart TB
+    A["UDP 数据报"] --> B{"目标地址类型"}
+    B -->|"具体IP"| C["单播:只有该主机收到"]
+    B -->|"255.255.255.255"| D["广播:同网段所有主机收到"]
+    B -->|"D 类地址 239.x.x.x"| E["组播:加入该组的主机收到"]
+```
+
+#### 2.3.2 UDP 单播与广播(10_udp_unicast_broadcast)
+
+流程:绑定本地端口 → 接收数据 → 向目标 IP 单播或向广播地址广播。
+
+**核心代码:**
+
+```cpp
+/* 绑定端口 */
+void MainWindow::bindPort()
+{
+    quint16 port = spinBox[0]->value();
+
+    /* 绑定端口需要在 socket 的状态为 UnconnectedState */
+    if (udpSocket->state() != QAbstractSocket::UnconnectedState)
+        udpSocket->close();
+
+    if (udpSocket->bind(port)) {
+        textBrowser->append("已经成功绑定端口:"
+                            + QString::number(port));
+
+        pushButton[0]->setEnabled(false);
+        pushButton[1]->setEnabled(true);
+        spinBox[1]->setEnabled(false);
+    }
+}
+
+/* 解绑端口 */
+void MainWindow::unbindPort()
+{
+    /* 解绑,不再监听 */
+    udpSocket->abort();
+
+    pushButton[0]->setEnabled(true);
+    pushButton[1]->setEnabled(false);
+    spinBox[1]->setEnabled(true);
+}
+
+/* 接收消息 */
+void MainWindow::receiveMessages()
+{
+    /* 局部变量,用于获取发送者的 IP 和端口 */
+    QHostAddress peerAddr;
+    quint16 peerPort;
+
+    /* 如果有数据已经准备好 */
+    while (udpSocket->hasPendingDatagrams()) {
+        /* udpSocket 发送的数据报是 QByteArray 类型的字节数组 */
+        QByteArray datagram;
+
+        /* 重新定义数组的大小 */
+        datagram.resize(udpSocket->pendingDatagramSize());
+
+        /* 读取数据,并获取发送方的 IP 地址和端口 */
+        udpSocket->readDatagram(datagram.data(),
+                                datagram.size(),
+                                &peerAddr,
+                                &peerPort);
+        /* 转为字符串 */
+        QString str = datagram.data();
+
+        /* 显示信息到文本浏览框窗口 */
+        textBrowser->append("接收来自"
+                            + peerAddr.toString()
+                            + ":"
+                            + QString::number(peerPort)
+                            + str);
+    }
+}
+
+/* 单播发送消息 */
+void MainWindow::sendMessages()
+{
+    /* 文本浏览框显示发送的信息 */
+    textBrowser->append("发送:" + lineEdit->text());
+
+    /* 要发送的信息,转为 QByteArray 类型字节数组,数据一般少于 512 个字节 */
+    QByteArray data = lineEdit->text().toUtf8();
+
+    /* 要发送的目标 Ip 地址 */
+    QHostAddress peerAddr = IPlist[comboBox->currentIndex()];
+
+    /* 要发送的目标端口号 */
+    quint16 peerPort = spinBox[1]->value();
+
+    /* 发送消息 */
+    udpSocket->writeDatagram(data, peerAddr, peerPort);
+}
+
+/* 广播发送消息 */
+void MainWindow::sendBroadcastMessages()
+{
+    textBrowser->append("发送:" + lineEdit->text());
+
+    QByteArray data = lineEdit->text().toUtf8();
+
+    /* 广播地址,一般为 255.255.255.255,
+     * 同一网段内监听目标端口的程序都会接收到消息 */
+    QHostAddress peerAddr = QHostAddress::Broadcast;
+
+    /* 要发送的目标端口号 */
+    quint16 peerPort = spinBox[1]->text().toInt();
+
+    /* 发送消息 */
+    udpSocket->writeDatagram(data, peerAddr, peerPort);
+}
+```
+
+**逐段解释:**
+
+- **`udpSocket->bind(port)`**:UDP 接收前必须绑定端口。绑定要求 socket 处于 `UnconnectedState`,否则先 `close()`。
+- **`hasPendingDatagrams()` + `pendingDatagramSize()`**:循环判断是否有待读数据报,并获取其大小。
+- **`readDatagram(data, size, &addr, &port)`**:读取数据报,同时可获取发送方 IP 与端口(这是 UDP 与 TCP 的重要区别)。
+- **`writeDatagram(data, addr, port)`**:发送数据报。单播传具体 IP,广播传 `QHostAddress::Broadcast`。
+- **`abort()`**:立即关闭 socket 并丢弃待处理数据,用于解绑。
+
+#### 2.3.3 UDP 组播(11_udp_multicast)
+
+组播的关键是**先绑定端口,再加入组播组**,发送时以组播地址和端口为目标。
+
+**核心代码:**
+
+```cpp
+/* 构造函数中设置组播 TTL */
+udpSocket = new QUdpSocket(this);
+/* 设置 IP_MULTICAST_TTL 套接字选项,限制数据包在 Internet 中的生存时间,
+ * 防止其无限期循环;数据报跨一个路由会减一,默认值为 1,表示多播仅适用于本地子网 */
+udpSocket->setSocketOption(QAbstractSocket::MulticastTtlOption, 1);
+
+/* 加入组播 */
+void MainWindow::joinGroup()
+{
+    /* 获取端口 */
+    quint16 port = spinBox->value();
+    /* 获取组播地址 */
+    QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());
+
+    /* 绑定端口需要在 socket 的状态为 UnconnectedState */
+    if (udpSocket->state() != QAbstractSocket::UnconnectedState)
+        udpSocket->close();
+
+    /* 加入组播前必须先绑定端口 */
+    if (udpSocket->bind(QHostAddress::AnyIPv4,
+                        port, QUdpSocket::ShareAddress)) {
+
+        /* 加入组播组,返回结果给 ok 变量 */
+        bool ok = udpSocket->joinMulticastGroup(groupAddr);
+
+        textBrowser->append(ok ? "加入组播成功" : "加入组播失败");
+        textBrowser->append("组播地址IP:" + comboBox[1]->currentText());
+        textBrowser->append("绑定端口:" + QString::number(port));
+
+        pushButton[0]->setEnabled(false);
+        pushButton[1]->setEnabled(true);
+        comboBox[1]->setEnabled(false);
+        spinBox->setEnabled(false);
+    }
+}
+
+/* 退出组播 */
+void MainWindow::leaveGroup()
+{
+    QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());
+
+    /* 退出组播 */
+    udpSocket->leaveMulticastGroup(groupAddr);
+
+    /* 解绑,不再监听 */
+    udpSocket->abort();
+
+    pushButton[0]->setEnabled(true);
+    pushButton[1]->setEnabled(false);
+    comboBox[1]->setEnabled(true);
+    spinBox->setEnabled(true);
+}
+
+/* 组播发送消息 */
+void MainWindow::sendMessages()
+{
+    textBrowser->append("发送:" + lineEdit->text());
+
+    QByteArray data = lineEdit->text().toUtf8();
+
+    /* 要发送的目标 Ip 地址(组播地址) */
+    QHostAddress groupAddr = QHostAddress(comboBox[1]->currentText());
+
+    /* 要发送的目标端口号 */
+    quint16 groupPort = spinBox->value();
+
+    /* 发送消息 */
+    udpSocket->writeDatagram(data, groupAddr, groupPort);
+}
+```
+
+**逐段解释:**
+
+- **`setSocketOption(QAbstractSocket::MulticastTtlOption, 1)`**:设置组播 TTL。默认 1 表示只在本地子网内多播。
+- **`bind(QHostAddress::AnyIPv4, port, QUdpSocket::ShareAddress)`**:绑定 `AnyIPv4` 表示监听所有 IPv4 网卡的该端口;`ShareAddress` 允许多个 socket 绑定同一地址端口(组播常用)。
+- **`joinMulticastGroup(groupAddr)`**:加入组播组,返回 bool。**必须先 bind 再 join**。
+- **`leaveMulticastGroup(groupAddr)`**:退出组播组,随后 `abort()` 解绑。
+- **`isLoopback()` 过滤**:组播例程在枚举本地 IP 时会过滤掉环回地址,保证显示的是实际网卡地址。
+
+#### 2.3.4 运行效果
+
+UDP 单播:服务端绑定端口,发送端填写目标 IP 和端口即可互发。广播:发送时选择广播,同一网段所有监听该端口的程序都能收到。组播:双方都加入同一组播地址(如 `239.255.255.1`)并绑定同一端口,即可互相收发。
+
+### 2.4 网络下载(12_imagedownload)
+
+例程使用 `QNetworkAccessManager` + `QNetworkRequest` + `QNetworkReply` 通过 HTTP 下载一张图片,显示进度并保存到本地。
+
+**核心代码:**
+
+```cpp
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QMessageBox>
+#include <QCoreApplication>
+
+void MainWindow::startDownload()
+{
+    /* 获取 URL 链接 */
+    QUrl newUrl(QUrl(lineEdit->text()));
+
+    /* 如果下载链接无效,则直接返回 */
+    if (!newUrl.isValid()) {
+        QMessageBox::information(this, "error", "invalid url");
+        return;
+    }
+
+    /* 网络请求 */
+    QNetworkRequest networkRequest;
+
+    /* 设置下载的地址 */
+    networkRequest.setUrl(newUrl);
+
+    /* 网络响应 */
+    QNetworkReply *newReply =
+            networkAccessManager->get(networkRequest);
+
+    /* 信号槽连接 */
+    connect(newReply, SIGNAL(finished()),
+            this, SLOT(replyFinished()));
+    connect(newReply, SIGNAL(downloadProgress(qint64, qint64)),
+            this, SLOT(imageDownloadProgress(qint64, qint64)));
+    connect(newReply,
+            SIGNAL(error(QNetworkReply::NetworkError)),
+            this,
+            SLOT(networkReplyError(QNetworkReply::NetworkError)));
+}
+
+void MainWindow::replyFinished()
+{
+    /* 获取信号发送者 */
+    QNetworkReply *reply = (QNetworkReply *)sender();
+
+    /* 防止内存泄漏 */
+    reply->deleteLater();
+
+    /* 立刻读取数据 */
+    readyReadData(reply);
+
+    /* 判断当前执行程序下的图像是否下载完成 */
+    QFile imageFile(QCoreApplication::applicationDirPath()
+                    + "/下载图.jpg");
+    if (imageFile.exists()) {
+        /* 显示下载的图片 */
+        label[0]->setPixmap(QPixmap(imageFile.fileName()));
+        qDebug() << "已经成功下载,文件路径为:"
+                 << imageFile.fileName() << endl;
+    } else
+        /* 清空显示 */
+        label[0]->clear();
+
+    /* 设置按钮可用 */
+    pushButton->setEnabled(true);
+}
+
+void MainWindow::readyReadData(QNetworkReply *reply)
+{
+    /* 设置按钮不可用,防止未完成,再次点击 */
+    pushButton->setEnabled(false);
+
+    QFile imageFile;
+    /* 保存到当前路径,名称为 "下载图.jpg" */
+    imageFile.setFileName(QCoreApplication::applicationDirPath()
+                          + "/下载图.jpg");
+
+    /* 如果此图片已经存在,则删除 */
+    if (imageFile.exists())
+        imageFile.remove();
+
+    /* 读取数据 */
+    QByteArray data = reply->readAll();
+    if (data.isEmpty()) {
+        qDebug() << "data is null, please try it again!" << endl;
+        return;
+    }
+
+    /* 判断是不是 JPG 格式的图片,如果不是则返回,如果不需要判断 JPG 可以删除这部分代码 */
+    if (!(data[0] == (char)0xff
+          && data[1] == (char)0xd8
+          && data[data.size() - 2] == (char)0xff
+          && data[data.size() - 1] == (char)0xd9)) {
+        qDebug() << "not JPG data, please try it again!" << endl;
+        return;
+    }
+
+    /* 转为 QPixmap */
+    QPixmap pixmap;
+    pixmap.loadFromData(data);
+    pixmap.save(imageFile.fileName());
+}
+
+void MainWindow::imageDownloadProgress(qint64 bytes,
+                                       qint64 totalBytes)
+{
+    /* 设置进度条的最大值 */
+    progressBar->setMaximum(totalBytes);
+    /* 设置当前值 */
+    progressBar->setValue(bytes);
+}
+
+/* 网络响应处理函数 */
+void MainWindow::networkReplyError(QNetworkReply::NetworkError error)
+{
+    switch (error) {
+    case QNetworkReply::ConnectionRefusedError:
+        qDebug() << "远程服务器拒绝连接" << endl;
+        break;
+    case QNetworkReply::HostNotFoundError:
+        qDebug() << "找不到远程主机名" << endl;
+        break;
+    case QNetworkReply::TimeoutError:
+        qDebug() << "与远程服务器连接超时" << endl;
+        break;
+    default:
+        break;
+    }
+}
+```
+
+**逐段解释:**
+
+- **`QNetworkAccessManager::get(request)`**:发起 HTTP GET 请求,返回一个 `QNetworkReply` 对象。请求是异步的,结果通过信号返回。
+- **`finished()` 信号**:请求完成(无论成功失败)后发出。
+- **`downloadProgress(qint64, qint64)`**:下载过程中持续发出,参数为已下载字节数和总字节数,用于更新进度条。
+- **`error(QNetworkReply::NetworkError)`**:出错时发出,可据此提示连接被拒、主机找不到、超时等。
+- **`reply->deleteLater()`**:响应对象需要手动释放,避免内存泄漏。
+- **`readAll()` 读取响应体**:请求完成后一次性读出全部数据;随后用 `loadFromData()` 转为 `QPixmap`。
+- **JPG 头尾校验**:JPG 文件以 `FF D8` 开头、`FF D9` 结尾,用于判断下载到的确实是图片而不是错误页面。
+- **保存路径用 `QCoreApplication::applicationDirPath()`**:可执行文件所在目录,避免相对路径歧义。
+
+**运行效果**:输入图片 URL(浏览器右键复制图片地址),点击「下载」,进度条随下载推进,完成后图片显示在窗口上方,并保存为可执行文件目录下的「下载图.jpg」。
+
+---
+
+## 3. 多媒体
+
+### 3.1 Qt 多媒体模块简介
+
+Qt 从 4.4 版本开始提供多媒体框架。Qt 4.6 中实现多媒体播放图形界面主要依赖 **phonon** 框架(源自 KDE,应用不用关心底层是 gstreamer 还是 xine,中间通过 backend 中转)。**从 Qt5 开始,Qt 弃用了 phonon,直接使用 Qt Multimedia 模块**,同样通过底层多媒体插件实现,但不再需要中转。
+
+Qt 多媒体模块的主要类:
+
+| 类 | 用途 |
+| -- | ---- |
+| `QSound` | 播放本地 WAV 音效(异步方式) |
+| `QSoundEffect` | 播放音效,支持本地与网络文件(用 `QUrl`) |
+| `QMediaPlayer` | 播放音乐/视频 |
+| `QMediaPlaylist` | 播放列表,管理多个媒体文件 |
+| `QVideoWidget` | 视频显示窗口 |
+| `QAudioRecorder` | 录音 |
+| `QAudioProbe` | 探测音频缓冲区(用于显示音量 level) |
+| `QAudioOutput` / `QAudioInput` | 音频输出/输入 |
+
+在 `.pro` 里添加:
+
+```pro
+QT += multimedia
+```
+
+> **注意**:Qt 的音乐/视频播放器需要在 Ubuntu 里安装媒体解码器才能播放。Ubuntu16/18 安装命令:
+>
+> ```bash
+> sudo apt-get install gstreamer1.0-plugins-base gstreamer1.0-plugins-bad \
+>   gstreamer1.0-plugins-good gstreamer1.0-plugins-ugly \
+>   gstreamer1.0-pulseaudio gstreamer1.0-libav
+> ```
+>
+> Windows 需要安装 LAVFilters 解码器。视频播放器还需要在 `.pro` 里额外加 `multimediawidgets`。
+
+### 3.2 音效文件播放(13_button_sound)
+
+播放简短提示音(按键音等)可以用 `QSoundEffect` 和 `QSound`。两者主要区别:
+
+- `QSound`:**异步方式播放,只能播放本地 WAV 音效文件**。WAV 文件尺寸较大,适合存储简短声音片段,具有低延时、不失真的特点。
+- `QSoundEffect`:不仅可播放本地音效,也可以播放网络文件(一般用 `QUrl` 链接)。
+
+例程 `13_button_sound`:点击按钮播放按键音效。
+
+```cpp
+/* mainwindow.h */
+#include <QMainWindow>
+#include <QSound>
+#include <QPushButton>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+private:
+    QPushButton *pushButton;
+private slots:
+    void pushButtonClicked();
+};
+```
+
+```cpp
+/* mainwindow.cpp */
+#include "mainwindow.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 设置主窗体的位置与大小 */
+    this->setGeometry(0, 0, 800, 480);
+
+    /* 实例化按钮 */
+    pushButton = new QPushButton(this);
+    pushButton->setText("按钮音效测试");
+    pushButton->setGeometry(340, 220, 120, 40);
+
+    connect(pushButton, SIGNAL(clicked()),
+            this, SLOT(pushButtonClicked()));
+}
+
+void MainWindow::pushButtonClicked()
+{
+    /* 异步的方式播放 */
+    QSound::play(":/audio/bell.wav");
+}
+```
+
+资源文件 `src.qrc`:
+
+```xml
+<RCC>
+    <qresource prefix="/">
+        <file>audio/bell.wav</file>
+    </qresource>
+</RCC>
+```
+
+**逐段解释**:
+
+- `QSound::play(fileName)` 是静态函数,直接播放指定音效,返回后立即继续执行(异步),不会阻塞界面。
+- 播放路径使用资源系统中的 `:/audio/bell.wav`,音效随可执行文件打包,开发板上无需额外拷贝文件。
+
+### 3.3 音乐播放器(14_musicplayer)
+
+例程 `14_musicplayer`:扫描 `myMusic` 目录下的 mp3,用 `QMediaPlayer` + `QMediaPlaylist` 播放,支持上一曲/下一曲、进度拖动、列表点击。
+
+**媒体核心结构(mainwindow.h):**
+
+```cpp
+#include <QMediaPlayer>
+#include <QMediaPlaylist>
+
+/* 媒体信息结构体 */
+struct MediaObjectInfo {
+    /* 用于保存歌曲文件名 */
+    QString fileName;
+    /* 用于保存歌曲文件路径 */
+    QString filePath;
+};
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+private:
+    /* 媒体播放器,用于播放音乐 */
+    QMediaPlayer *musicPlayer;
+    /* 媒体列表 */
+    QMediaPlaylist *mediaPlaylist;
+    /* 音乐列表 */
+    QListWidget *listWidget;
+    /* 播放进度条 */
+    QSlider *durationSlider;
+    QPushButton *pushButton[7];
+    QLabel *label[4];
+    QWidget *listMask;
+    QVector<MediaObjectInfo> mediaObjectInfo;
+
+    /* 音乐布局函数(界面构建,代码较长,见例程) */
+    void musicLayout();
+    /* 主窗体大小重设大小函数重写 */
+    void resizeEvent(QResizeEvent *event);
+    /* 扫描歌曲 */
+    void scanSongs();
+    /* 媒体播放器类初始化 */
+    void mediaPlayerInit();
+private slots:
+    void btn_play_clicked();
+    void btn_next_clicked();
+    void btn_previous_clicked();
+    void mediaPlayerStateChanged(QMediaPlayer::State);
+    void listWidgetCliked(QListWidgetItem*);
+    void mediaPlaylistCurrentIndexChanged(int);
+    void musicPlayerDurationChanged(qint64);
+    void mediaPlayerPositionChanged(qint64);
+    void durationSliderReleased();
+};
+```
+
+**播放器初始化与扫描歌曲:**
+
+```cpp
+void MainWindow::mediaPlayerInit()
+{
+    musicPlayer = new QMediaPlayer(this);
+    mediaPlaylist = new QMediaPlaylist(this);
+    /* 确保列表是空的 */
+    mediaPlaylist->clear();
+    /* 设置音乐播放器的列表为 mediaPlaylist */
+    musicPlayer->setPlaylist(mediaPlaylist);
+    /* 设置播放模式,Loop 是列表循环 */
+    mediaPlaylist->setPlaybackMode(QMediaPlaylist::Loop);
+}
+
+void MainWindow::scanSongs()
+{
+    QDir dir(QCoreApplication::applicationDirPath() + "/myMusic");
+    QDir dirbsolutePath(dir.absolutePath());
+    /* 如果目录存在 */
+    if (dirbsolutePath.exists()) {
+        /* 定义过滤器 */
+        QStringList filter;
+        /* 包含所有 mp3 后缀的文件 */
+        filter << "*.mp3";
+        /* 获取该目录下的所有文件 */
+        QFileInfoList files =
+                dirbsolutePath.entryInfoList(filter, QDir::Files);
+        /* 遍历 */
+        for (int i = 0; i < files.count(); i++) {
+            MediaObjectInfo info;
+            /* 使用 utf-8 编码 */
+            QString fileName = QString::fromUtf8(files.at(i)
+                                                 .fileName()
+                                                 .replace(".mp3", "")
+                                                 .toUtf8()
+                                                 .data());
+            info.fileName = fileName + "\n"
+                    + fileName.split("-").at(1);
+            info.filePath = QString::fromUtf8(files.at(i)
+                                              .filePath()
+                                              .toUtf8()
+                                              .data());
+            /* 媒体列表添加歌曲 */
+            if (mediaPlaylist->addMedia(
+                        QUrl::fromLocalFile(info.filePath))) {
+                mediaObjectInfo.append(info);
+                listWidget->addItem(info.fileName);
+            } else {
+                qDebug() << mediaPlaylist->errorString().toUtf8().data();
+                qDebug() << "  Error number:" << mediaPlaylist->error();
+            }
+        }
+    }
+}
+```
+
+**播放控制:**
+
+```cpp
+void MainWindow::btn_play_clicked()
+{
+    int state = musicPlayer->state();
+    switch (state) {
+    case QMediaPlayer::StoppedState:
+        musicPlayer->play();
+        break;
+    case QMediaPlayer::PlayingState:
+        musicPlayer->pause();
+        break;
+    case QMediaPlayer::PausedState:
+        musicPlayer->play();
+        break;
+    }
+}
+
+void MainWindow::btn_next_clicked()
+{
+    musicPlayer->stop();
+    int count = mediaPlaylist->mediaCount();
+    if (0 == count)
+        return;
+    /* 列表下一曲 */
+    mediaPlaylist->next();
+    musicPlayer->play();
+}
+
+void MainWindow::btn_previous_clicked()
+{
+    musicPlayer->stop();
+    int count = mediaPlaylist->mediaCount();
+    if (0 == count)
+        return;
+    /* 列表上一曲 */
+    mediaPlaylist->previous();
+    musicPlayer->play();
+}
+
+void MainWindow::listWidgetCliked(QListWidgetItem *item)
+{
+    musicPlayer->stop();
+    mediaPlaylist->setCurrentIndex(listWidget->row(item));
+    musicPlayer->play();
+}
+
+void MainWindow::durationSliderReleased()
+{
+    /* 设置媒体播放的位置 */
+    musicPlayer->setPosition(durationSlider->value() * 1000);
+}
+```
+
+**时长与进度显示:**
+
+```cpp
+void MainWindow::musicPlayerDurationChanged(qint64 duration)
+{
+    durationSlider->setRange(0, duration / 1000);
+    int second = duration / 1000;
+    int minute = second / 60;
+    second %= 60;
+
+    QString mediaDuration;
+    if (minute >= 10)
+        mediaDuration = QString::number(minute, 10);
+    else
+        mediaDuration = "0" + QString::number(minute, 10);
+
+    if (second >= 10)
+        mediaDuration = mediaDuration + ":" + QString::number(second, 10);
+    else
+        mediaDuration = mediaDuration + ":0" + QString::number(second, 10);
+
+    /* 显示媒体总长度时间 */
+    label[3]->setText(mediaDuration);
+}
+
+void MainWindow::mediaPlayerPositionChanged(qint64 position)
+{
+    if (!durationSlider->isSliderDown())
+        durationSlider->setValue(position / 1000);
+
+    int second = position / 1000;
+    int minute = second / 60;
+    second %= 60;
+
+    QString mediaPosition;
+    if (minute >= 10)
+        mediaPosition = QString::number(minute, 10);
+    else
+        mediaPosition = "0" + QString::number(minute, 10);
+
+    if (second >= 10)
+        mediaPosition = mediaPosition + ":" + QString::number(second, 10);
+    else
+        mediaPosition = mediaPosition + ":0" + QString::number(second, 10);
+
+    /* 显示现在播放的时间 */
+    label[2]->setText(mediaPosition);
+}
+```
+
+**信号槽连接(构造函数中):**
+
+```cpp
+connect(pushButton[0], SIGNAL(clicked()), this, SLOT(btn_previous_clicked()));
+connect(pushButton[1], SIGNAL(clicked()), this, SLOT(btn_play_clicked()));
+connect(pushButton[2], SIGNAL(clicked()), this, SLOT(btn_next_clicked()));
+
+connect(musicPlayer, SIGNAL(stateChanged(QMediaPlayer::State)),
+        this, SLOT(mediaPlayerStateChanged(QMediaPlayer::State)));
+connect(mediaPlaylist, SIGNAL(currentIndexChanged(int)),
+        this, SLOT(mediaPlaylistCurrentIndexChanged(int)));
+connect(musicPlayer, SIGNAL(durationChanged(qint64)),
+        this, SLOT(musicPlayerDurationChanged(qint64)));
+connect(musicPlayer, SIGNAL(positionChanged(qint64)),
+        this, SLOT(mediaPlayerPositionChanged(qint64)));
+connect(listWidget, SIGNAL(itemClicked(QListWidgetItem*)),
+        this, SLOT(listWidgetCliked(QListWidgetItem*)));
+connect(durationSlider, SIGNAL(sliderReleased()),
+        this, SLOT(durationSliderReleased()));
+```
+
+**逐段解释**:
+
+- `QMediaPlayer` 负责播放,`QMediaPlaylist` 负责曲目列表与切换。`musicPlayer->setPlaylist(mediaPlaylist)` 把它们绑定起来。
+- `setPlaybackMode(QMediaPlaylist::Loop)` 设置循环播放模式。
+- `addMedia(QUrl::fromLocalFile(path))` 添加本地媒体,返回是否成功。
+- 播放状态由 `stateChanged` 信号报告,界面用 `pushButton[1]->setChecked()` 同步按钮的按下/弹起外观。
+- 播放进度:`positionChanged` 持续报告当前播放位置,用于更新时间和滑块;`durationChanged` 报告总时长。拖动滑块时先 `isSliderDown()` 判断,避免自动更新与用户拖动冲突;松开后 `setPosition()` 跳转。
+- 界面布局函数 `musicLayout()` 负责按钮、列表、CD 图片、进度条等的摆放,代码较长,直接参考例程即可。
+
+**运行效果**:程序目录下的 `myMusic` 文件夹中的 mp3 显示在左侧列表;点击列表项或播放/上一曲/下一曲按钮进行播放,底部显示当前时间/总时长,拖动进度条可跳转。
+
+### 3.4 视频播放器(15_videoplayer)
+
+例程 `15_videoplayer`:用 `QMediaPlayer` + `QMediaPlaylist` + `QVideoWidget` 播放 `myVideo` 目录下的视频,支持播放/暂停、下一集、音量加减、全屏。
+
+关键区别:视频需要设置视频输出窗口,并在 `.pro` 里额外加 `multimediawidgets`。
+
+```pro
+QT += core gui multimedia multimediawidgets
+```
+
+**媒体初始化(与音乐播放器的关键差异):**
+
+```cpp
+#include <QVideoWidget>
+
+void MainWindow::mediaPlayerInit()
+{
+    videoPlayer = new QMediaPlayer(this);
+    mediaPlaylist = new QMediaPlaylist(this);
+    /* 确保列表是空的 */
+    mediaPlaylist->clear();
+    /* 设置视频播放器的列表为 mediaPlaylist */
+    videoPlayer->setPlaylist(mediaPlaylist);
+    /* 设置视频输出窗口——这是视频播放与音频播放的关键差别 */
+    videoPlayer->setVideoOutput(videoWidget);
+    /* 设置播放模式,Loop 是列表循环 */
+    mediaPlaylist->setPlaybackMode(QMediaPlaylist::Loop);
+    /* 设置默认软件音量 50% */
+    videoPlayer->setVolume(50);
+}
+```
+
+**扫描视频文件:**
+
+```cpp
+void MainWindow::scanVideoFiles()
+{
+    QDir dir(QCoreApplication::applicationDirPath() + "/myVideo");
+    QDir dirbsolutePath(dir.absolutePath());
+    if (dirbsolutePath.exists()) {
+        QStringList filter;
+        /* 包含所有常见视频后缀的文件 */
+        filter << "*.mp4" << "*.mkv" << "*.wmv" << "*.avi";
+        QFileInfoList files =
+                dirbsolutePath.entryInfoList(filter, QDir::Files);
+        for (int i = 0; i < files.count(); i++) {
+            MediaObjectInfo info;
+            info.fileName = QString::fromUtf8(files.at(i)
+                                              .fileName().toUtf8().data());
+            info.filePath = QString::fromUtf8(files.at(i)
+                                              .filePath().toUtf8().data());
+            if (mediaPlaylist->addMedia(
+                        QUrl::fromLocalFile(info.filePath))) {
+                mediaObjectInfo.append(info);
+                listWidget->addItem(info.fileName);
+            } else {
+                qDebug() << mediaPlaylist->errorString().toUtf8().data();
+                qDebug() << "  Error number:" << mediaPlaylist->error();
+            }
+        }
+    }
+}
+```
+
+**播放与音量控制:**
+
+```cpp
+void MainWindow::btn_play_clicked()
+{
+    int state = videoPlayer->state();
+    switch (state) {
+    case QMediaPlayer::StoppedState:
+        videoPlayer->play();
+        break;
+    case QMediaPlayer::PlayingState:
+        videoPlayer->pause();
+        break;
+    case QMediaPlayer::PausedState:
+        videoPlayer->play();
+        break;
+    }
+}
+
+void MainWindow::btn_next_clicked()
+{
+    videoPlayer->stop();
+    int count = mediaPlaylist->mediaCount();
+    if (0 == count)
+        return;
+    mediaPlaylist->next();
+    videoPlayer->play();
+}
+
+void MainWindow::btn_volmeup_clicked()
+{
+    /* 点击每次音量 +5 */
+    volumeSlider->setValue(volumeSlider->value() + 5);
+    videoPlayer->setVolume(volumeSlider->value());
+}
+
+void MainWindow::btn_volmedown_clicked()
+{
+    /* 点击每次音量 -5 */
+    volumeSlider->setValue(volumeSlider->value() - 5);
+    videoPlayer->setVolume(volumeSlider->value());
+}
+
+void MainWindow::btn_fullscreen_clicked()
+{
+    /* 全屏/非全屏操作,隐藏/显示左侧列表容器 */
+    vWidget[0]->setVisible(!pushButton[4]->isChecked());
+}
+
+void MainWindow::volumeSliderReleased()
+{
+    videoPlayer->setVolume(volumeSlider->value());
+}
+```
+
+**逐段解释**:
+
+- `videoPlayer->setVideoOutput(videoWidget)` 把视频画面输出到 `QVideoWidget`,这是视频与音乐播放器最本质的区别。
+- `setVolume(0~100)` 设置音量,配合 `QSlider` 实现音量条;按钮点击时 ±5。
+- 全屏通过隐藏左侧列表容器实现,`resizeEvent()` 重写保证底部控制条随窗口大小重新布局。
+- 进度条拖动逻辑与音乐播放器相同(`durationChanged` / `positionChanged` / `sliderReleased`)。
+
+**运行效果**:左侧显示视频列表,右侧 `QVideoWidget` 播放视频,底部有进度条与播放、下一集、音量、全屏按钮;音量条拖动实时生效,全屏按钮隐藏列表。
+
+### 3.5 录音(16_audiorecorder)
+
+例程 `16_audiorecorder`:用 `QAudioRecorder` 录音为 mp3,用 `QAudioProbe` 实时探测音频缓冲区显示音量 level,并支持回放、上一首/下一首、删除录音。
+
+**录音数据结构(audiorecorder.h 节选):**
+
+```cpp
+#include <QAudioRecorder>
+#include <QAudioProbe>
+#include <QAudioBuffer>
+#include <QMediaPlaylist>
+#include <QMediaPlayer>
+#include <QProgressBar>
+
+class AudioRecorder : public QMainWindow
+{
+    Q_OBJECT
+public:
+    AudioRecorder(QWidget *parent = nullptr);
+    ~AudioRecorder();
+private:
+    void layoutInit();
+    QAudioRecorder *m_audioRecorder = nullptr;
+    QAudioProbe *m_probe = nullptr;
+    QMediaPlayer *recorderPlayer;
+    QMediaPlaylist *mediaPlaylist;
+    QProgressBar *progressBar[4];
+    QVector<MediaObjectInfo> mediaObjectInfo;
+
+    /* 录音设置容器,保存录音设备的可用信息 */
+    QList<QVariant> devicesVar, codecsVar, containersVar,
+                    sampleRateVar, channelsVar, qualityVar, bitratesVar;
+
+    void scanRecordFiles();
+    void clearAudioLevels();
+private slots:
+    void recorderBtClicked();
+    void processBuffer(const QAudioBuffer&);
+    void updateProgress(qint64);
+    // ... 播放相关槽 ...
+};
+```
+
+**录音初始化(构造函数节选):**
+
+```cpp
+/* 录制音频的类 */
+m_audioRecorder = new QAudioRecorder(this);
+
+/* 用于探测缓冲区的数据 */
+m_probe = new QAudioProbe(this);
+
+/* 信号槽连接,更新录音 level 显示 */
+connect(m_probe, &QAudioProbe::audioBufferProbed,
+        this, &AudioRecorder::processBuffer);
+
+/* 设置探测的对象 */
+m_probe->setSource(m_audioRecorder);
+
+/* 播放器与播放列表 */
+recorderPlayer = new QMediaPlayer(this);
+mediaPlaylist = new QMediaPlaylist(this);
+recorderPlayer->setPlaylist(mediaPlaylist);
+mediaPlaylist->setPlaybackMode(QMediaPlaylist::CurrentItemOnce);
+
+/* 扫描本地声卡设备,保存到 devicesVar */
+devicesVar.append(QVariant(QString()));
+for (auto &device : m_audioRecorder->audioInputs())
+    devicesVar.append(QVariant(device));
+
+/* 支持的音频编码 */
+for (auto &codecName : m_audioRecorder->supportedAudioCodecs())
+    codecsVar.append(QVariant(codecName));
+
+/* 支持的容器格式 */
+for (auto &containerName : m_audioRecorder->supportedContainers())
+    containersVar.append(QVariant(containerName));
+
+/* 支持的采样率 */
+for (int sampleRate : m_audioRecorder->supportedAudioSampleRates())
+    sampleRateVar.append(QVariant(sampleRate));
+
+/* 录音时长变化 */
+connect(m_audioRecorder, &QAudioRecorder::durationChanged,
+        this, &AudioRecorder::updateProgress);
+```
+
+**开始/停止录音:**
+
+```cpp
+void AudioRecorder::recorderBtClicked()
+{
+    /* 录音前停止正在播放的媒体 */
+    if (recorderPlayer->state() != QMediaPlayer::StoppedState)
+        recorderPlayer->stop();
+
+    /* 如果录音已经停止,则开始录音 */
+    if (m_audioRecorder->state() == QMediaRecorder::StoppedState) {
+        /* 设置默认的录音设备 */
+        m_audioRecorder->setAudioInput(devicesVar.at(0).toString());
+
+        /* 下面的是录音设置,都是选择默认,可根据录音可用项自行修改 */
+        QAudioEncoderSettings settings;
+        settings.setCodec(codecsVar.at(0).toString());
+        settings.setSampleRate(sampleRateVar[0].toInt());
+        settings.setBitRate(bitratesVar[0].toInt());
+        settings.setChannelCount(channelsVar[0].toInt());
+        settings.setQuality(QMultimedia::EncodingQuality(
+                                qualityVar[0].toInt()));
+        /* 以恒定的质量录制,可选恒定的比特率 */
+        settings.setEncodingMode(QMultimedia::ConstantQualityEncoding);
+        QString container = containersVar.at(0).toString();
+        m_audioRecorder->setEncodingSettings(settings,
+                                             QVideoEncoderSettings(),
+                                             container);
+        m_audioRecorder->setOutputLocation(
+            QUrl::fromLocalFile(
+                QCoreApplication::applicationDirPath()
+                + tr("/Sounds/%1.mp3")
+                  .arg(QDateTime::currentDateTime().toString())));
+        /* 开始录音 */
+        m_audioRecorder->record();
+        countLabel->clear();
+        centerWidget->show();
+    } else {
+        /* 停止录音 */
+        m_audioRecorder->stop();
+        clearAudioLevels();
+        centerWidget->hide();
+        /* 重新扫描录音文件 */
+        scanRecordFiles();
+    }
+}
+
+/* 录音时长更新 */
+void AudioRecorder::updateProgress(qint64 duration)
+{
+    if (m_audioRecorder->error() != QMediaRecorder::NoError)
+        return;
+    /* 显示录制时长 */
+    countLabel->setText(tr("已录制 %1 s").arg(duration / 1000));
+}
+```
+
+**扫描录音文件(与音乐播放器类似):**
+
+```cpp
+void AudioRecorder::scanRecordFiles()
+{
+    mediaPlaylist->clear();
+    listWidget->clear();
+    mediaObjectInfo.clear();
+
+    /* 录音文件保存在当前 Sounds 文件夹下 */
+    QDir dir(QCoreApplication::applicationDirPath() + "/Sounds");
+    QDir dirbsolutePath(dir.absolutePath());
+
+    /* 如果文件夹不存在,则创建一个 */
+    if (!dirbsolutePath.exists())
+        dirbsolutePath.mkdir(dirbsolutePath.absolutePath());
+
+    QStringList filter;
+    filter << "*.mp3";
+    QFileInfoList files =
+            dirbsolutePath.entryInfoList(filter, QDir::Files);
+    for (int i = 0; i < files.count(); i++) {
+        MediaObjectInfo info;
+        info.fileName = QString::fromUtf8(
+                    files.at(i).fileName().toUtf8().data());
+        info.filePath = QString::fromUtf8(
+                    files.at(i).filePath().toUtf8().data());
+        if (mediaPlaylist->addMedia(
+                    QUrl::fromLocalFile(info.filePath))) {
+            mediaObjectInfo.append(info);
+            listWidget->addItem(new QListWidgetItem(
+                                    QIcon(":/icons/play.png"),
+                                    info.fileName));
+        } else {
+            qDebug() << mediaPlaylist->errorString().toUtf8().data();
+        }
+    }
+}
+```
+
+**音量 level 探测(QAudioProbe → QAudioBuffer → 进度条):**
+
+```cpp
+void AudioRecorder::processBuffer(const QAudioBuffer &buffer)
+{
+    /* 根据通道数目需要显示 count 个 level */
+    int count = buffer.format().channelCount();
+    for (int i = 0; i < 4; i++) {
+        if (i < count)
+            progressBar[i]->setVisible(true);
+        else
+            progressBar[i]->setVisible(false);
+    }
+
+    /* 设置 level 的值 */
+    QVector<qreal> levels = getBufferLevels(buffer);
+    for (int i = 0; i < levels.count(); ++i)
+        progressBar[i]->setValue(levels.at(i) * 100);
+}
+```
+
+`getBufferLevels()` 会判断采样类型(`Float`/`SignedInt`/`UnSignedInt`)、采样位宽(8/16/32 位),用 `getPeakValue()` 求出该格式的最大采样值,再计算各通道的峰值并归一化到 0~1,最后乘以 100 作为进度条的值。
+
+**逐段解释**:
+
+- `QAudioRecorder` 是录音核心,负责选择输入设备、编码设置和输出文件。
+- `QAudioEncoderSettings` 设置编码器、采样率、比特率、通道数、质量与编码模式;`QVideoEncoderSettings()`(空)与 `container` 一起传给 `setEncodingSettings()`。
+- `setOutputLocation(QUrl::fromLocalFile(...))` 指定输出文件,这里用当前日期时间命名,保存到 `Sounds` 目录。
+- `QAudioProbe::audioBufferProbed` 信号在每次音频缓冲区就绪时发出,槽函数 `processBuffer()` 计算音量并更新 level 进度条,实现「录音时音量条跳动」的效果。
+- `state() == QMediaRecorder::StoppedState` 用来区分「开始录音」和「停止录音」两种点击。
+
+**运行效果**:点击录音按钮开始录音,窗口中央显示已录制时长与跳动的音量条;再次点击停止并刷新列表。点击列表项播放录音,可上一首/下一首/删除(删除会同时删除文件与列表项)。首次运行会自动创建 `Sounds` 目录。
+
+---
+
+## 4. 数据库
+
+### 4.1 Qt SQL 模块简介
+
+**数据库是什么?** 简单说就是保存数据的文件,可以存储大量数据,支持插入、更新、删除等操作。专业上说,数据库是「按照数据结构来组织、存储和管理数据的仓库」。
+
+**什么时候需要数据库?** 在嵌入式里,需要存储大量数据或记录数据时就需要。例如手机的闹钟:设置的闹钟数据保存到数据库,程序重新运行时会从数据库读回上次保存的数据;如果没有数据库,关机后数据丢失,明显不合理。
+
+Qt SQL 模块为数据库提供编程支持,支持 MySQL、Oracle、MS SQL Server、SQLite 等。它包含连接数据库、执行 SQL、获取数据与界面显示等类,一般数据与界面之间采用 **Model/View 架构**。
+
+**嵌入式里一般用 SQLite3**:非常小、轻量级,完全配置时小于 400KiB,省略可选功能时小于 250KiB。它是一个进程内的库,实现了自给自足的、无服务器的、零配置的、事务性的 SQL 数据库引擎。SQLite 引擎不是独立进程,可按应用需求静态或动态连接,直接访问其存储文件。
+
+在 `.pro` 里添加:
+
+```pro
+QT += core gui sql
+```
+
+### 4.2 Qt 数据库操作流程
+
+Qt 对数据库的基本操作流程:
+
+1. `QSqlDatabase` 加载某一类型的数据库驱动后,用 `open()` 打开(连接)数据库;
+2. 用 `QSqlQuery` 创建数据库表,用 `exec()` 执行增删查改 SQL;
+3. 通过 `QTableView` 显示数据库内容:设置 Model 为 `QSqlTableModel`(或 `QSqlQueryModel`);
+4. 直接编辑 `QTableView` 里的内容也可以修改数据库表格内容。
+
+```mermaid
+flowchart LR
+    A["QSqlDatabase<br/>加载驱动 + open()"] --> B["QSqlQuery<br/>exec() 建表/增删查改"]
+    B --> C["QSqlTableModel / QSqlQueryModel<br/>读取表格模型"]
+    C --> D["QTableView / QListWidget<br/>显示与编辑"]
+```
+
+### 4.3 Qt SQL 核心类与 API
+
+| 类 | 作用 | 常用 API |
+| -- | ---- | -------- |
+| `QSqlDatabase` | 数据库连接 | `addDatabase(driver)`、`setDatabaseName(name)`、`open()`、`close()`、`lastError()`、`drivers()` |
+| `QSqlQuery` | 执行 SQL 语句 | `exec(sql)`、`prepare(sql)`、`bindValue(placeholder, value)`、`first()`、`next()`、`value(field)` |
+| `QSqlTableModel` | 单表模型,可编辑 | `setTable()`、`setEditStrategy()`、`select()`、`insertRow()`、`setData()`、`data()`、`submit()`、`submitAll()`、`removeRow()`、`isDirty()`、`rowCount()` |
+| `QSqlQueryModel` | 查询结果模型,只读 | `setQuery()`、`record()`、`setHeaderData()`、`lastError()` |
+| `QDataWidgetMapper` | 模型字段映射到控件 | `setModel()`、`setSubmitPolicy()`、`addMapping()`、`setCurrentModelIndex()` |
+| `QItemSelectionModel` | 选择模型 | `currentRowChanged` 信号、`currentIndex()` |
+| `QTableView` | 表格视图 | `setModel()`、`setSelectionModel()`、`hideColumn()`、`horizontalHeader()` |
+
+`QSqlTableModel::EditStrategy` 三种编辑策略:
+
+| 策略 | 说明 |
+| ---- | ---- |
+| `OnFieldChange` | 字段改变即同步到数据库 |
+| `OnRowChange` | 行改变(切换到别的行)时同步 |
+| `OnManualSubmit` | 手动调用 `submit()` / `submitAll()` 才提交 |
+
+> **注意**:`setEditStrategy(OnFieldChange)` 这类规则需要与 `QTableView` 这样的控件配合才生效,因为 `QTableView` 可以直接编辑表里的内容。
+
+### 4.4 实用闹钟(17_sqlite_alarm)
+
+例程 `17_sqlite_alarm`:结合数据库开发一个闹钟,记录新建的闹钟数据,支持增、删、改操作,但不使用 `QTableView` 显示(用 `QListWidget` + 自定义开关),界面与手机闹钟类似。本例篇幅很长,只关注 `mainwindow.h` 和 `mainwindow.cpp` 的数据库操作;`numberpicker.*`(数字选择器)和 `switchbutton.*`(闹钟开关)是为界面美观自行设计的自定义控件,可直接当普通控件使用。
+
+**数据库初始化与建表(构造函数节选):**
+
+```cpp
+#include <QSqlDatabase>
+#include <QSqlQuery>
+#include <QSqlTableModel>
+#include <QSqlError>
+
+/* 查看本机可用的数据库驱动 */
+QStringList drivers = QSqlDatabase::drivers();
+foreach (QString driver, drivers) {
+    qDebug() << driver;
+}
+
+/* 以 QSQLITE 驱动方式打开或者创建数据库 */
+sqlDatabase = QSqlDatabase::addDatabase("QSQLITE");
+sqlDatabase.setDatabaseName("alarm.db");
+/* 以 open 的方式打开 alarm.db 数据库,则会创建一个 alarm.db */
+if (!sqlDatabase.open())
+    qDebug() << "连接数据库错误" << sqlDatabase.lastError() << endl;
+else
+    qDebug() << "连接数据库成功" << endl;
+
+QSqlQuery query(sqlDatabase);
+/* 使用指令式创建表 */
+query.exec("create table alarm (id int primary key, time vchar(15), flag vchar(5))");
+
+model = new QSqlTableModel(this, sqlDatabase);
+
+/* 模型设置表的名字,需要与数据库的表的名字相同 */
+model->setTable("alarm");
+
+/* 如果有修改则同步修改到数据库,
+ * 注意这个规则需要与 tableView 这样的控件才生效 */
+model->setEditStrategy(QSqlTableModel::OnFieldChange);
+
+/* 成功则返回 true,查看数据库里是否有 alarm 这个表格 */
+model->select();
+
+/* 如果数据表数据为空,则添加两个闹钟 */
+if (model->rowCount() == 0) {
+    /* 插入一行 */
+    model->insertRow(model->rowCount());
+    /* 在该行插入数据 */
+    model->setData(model->index(0, 0), 1);
+    model->setData(model->index(0, 1), "06:00");
+    model->setData(model->index(0, 2), "false");
+    /* 插入数据后记得提交 */
+    model->submit();
+
+    /* 再插入一行 */
+    model->insertRow(model->rowCount());
+    model->setData(model->index(1, 0), 2);
+    model->setData(model->index(1, 1), "18:00");
+    model->setData(model->index(1, 2), "true");
+    model->submit();
+}
+```
+
+**读数据到列表(构造函数节选):**
+
+```cpp
+/* 打印出闹钟数据库里的信息并添加到列表 */
+for (int i = 0; i < model->rowCount(); i++) {
+    for (int j = 0; j < 3; j++) {
+        QModelIndex qindex = model->index(i, j);
+        switch (j) {
+        case 0:
+            qDebug() << "第" << model->data(qindex).toInt() << "行数据";
+            break;
+        case 1:
+            listWidget->addItem(model->data(qindex).toString());
+            qDebug() << "闹钟时间为:" << model->data(qindex).toString();
+            break;
+        case 2:
+            qDebug() << "闹钟状态为:" << model->data(qindex).toString();
+            if (model->data(qindex).toString() != "true")
+                listWidget->item(i)->setTextColor(QColor(22, 22, 22, 60));
+            else
+                listWidget->item(i)->setTextColor(QColor(22, 22, 22, 225));
+            break;
+        }
+    }
+}
+
+/* 在列表里添加闹钟开关 */
+for (int i = 0; i < model->rowCount(); i++) {
+    ItemObjectInfo info;
+    info.widget = new QWidget();
+    info.switchButton = new SwitchButton();
+    info.hBoxLayout = new QHBoxLayout();
+    info.switchButton->setMaximumSize(55, 30);
+    info.switchButton->setMinimumSize(55, 30);
+    info.hBoxLayout->setContentsMargins(0, 0, 0, 0);
+    info.hBoxLayout->setAlignment(Qt::AlignRight);
+    info.hBoxLayout->addWidget(info.switchButton);
+    info.widget->setLayout(info.hBoxLayout);
+    listWidget->setItemWidget(listWidget->item(i), info.widget);
+    itemObjectInfo.append(info);
+
+    connect(info.switchButton, SIGNAL(toggled(bool)),
+            this, SLOT(switchButtonClicked(bool)));
+
+    /* 获取数据库里的闹钟开关状态 */
+    QModelIndex qindex = model->index(i, 2);
+    if (model->data(qindex).toBool())
+        info.switchButton->setToggle(true);
+}
+```
+
+**新增/修改闹钟(确认按钮):**
+
+```cpp
+void MainWindow::yesButtonClicked()
+{
+    /* 获取数值选择值的数据,转为字符串 */
+    QString hour;
+    QString minute;
+
+    if (hourPicker->readValue() < 10)
+        hour = "0" + QString::number(hourPicker->readValue()) + ":";
+    else
+        hour = QString::number(hourPicker->readValue()) + ":";
+
+    if (minutePicker->readValue() < 10)
+        minute = "0" + QString::number(minutePicker->readValue());
+    else
+        minute = QString::number(minutePicker->readValue());
+
+    /* 如果不是选中闹钟列表的数据 */
+    if (listWidget->currentRow() == -1) {
+        /* 插入一行数据,闹钟时间为选择的闹钟时间 */
+        int row = model->rowCount();
+
+        /* 插入数据到数据库 */
+        model->insertRow(row);
+        model->setData(model->index(row, 0), row + 1);
+        model->setData(model->index(row, 1), hour + minute);
+        model->setData(model->index(row, 2), "true");
+        model->submit();
+
+        /* 添加闹钟到列表(含开关控件,略) */
+        listWidget->addItem(hour + minute);
+        // ... 创建 SwitchButton 并 setItemWidget ...
+    } else {
+        /* 修改数据(更新闹钟数据) */
+        int row = listWidget->currentRow();
+        model->setData(model->index(row, 0), row + 1);
+        model->setData(model->index(row, 1), hour + minute);
+        model->setData(model->index(row, 2), "true");
+        model->submit();
+
+        /* 设置当前项的闹钟文本 */
+        listWidget->currentItem()->setText(hour + minute);
+    }
+
+    /* 再确保提交 */
+    if (model->isDirty())
+        model->submitAll();
+
+    /* 关闭对话框 */
+    alarmDialog->close();
+}
+```
+
+**删除闹钟(取消按钮在"删除"模式):**
+
+```cpp
+void MainWindow::cancelButtonClicked()
+{
+    if (cancelButton->text() == "删除") {
+        /* 删除数据库整一行数据 */
+        model->removeRow(listWidget->currentRow());
+        model->submit();
+        /* 重新查询刷新模型 */
+        model->select();
+        itemObjectInfo.remove(listWidget->currentRow());
+        listWidget->takeItem(listWidget->currentRow());
+    }
+
+    if (model->isDirty())
+        model->submitAll();
+
+    alarmDialog->close();
+}
+```
+
+**开关状态同步到数据库:**
+
+```cpp
+void MainWindow::switchButtonClicked(bool checked)
+{
+    listWidget->clearSelection();
+
+    SwitchButton *button = (SwitchButton *)sender();
+    for (int i = 0; i < itemObjectInfo.count(); i++) {
+        if (button == itemObjectInfo.at(i).switchButton) {
+            if (checked) {
+                model->setData(model->index(i, 2), "true");
+                listWidget->item(i)->setTextColor(QColor(22, 22, 22, 225));
+            } else {
+                model->setData(model->index(i, 2), "false");
+                listWidget->item(i)->setTextColor(QColor(22, 22, 22, 60));
+            }
+            model->submit();
+            break;
+        }
+    }
+}
+```
+
+**逐段解释**:
+
+- `QSqlDatabase::addDatabase("QSQLITE")` 加载 SQLite 驱动,`setDatabaseName("alarm.db")` 指定数据库文件名,`open()` 打开——文件不存在会自动创建。
+- `QSqlQuery::exec()` 直接执行 SQL 建表语句;字段 `flag` 保存闹钟开关状态(`"true"` / `"false"`)。
+- `QSqlTableModel` 是单表模型,`setTable("alarm")` 指定表名,`select()` 查询;`rowCount()`、`data()`、`setData()` 操作模型数据。
+- **`setData()` 后要 `submit()`**:`setData()` 只改模型,`submit()` 才写回数据库(`OnFieldChange` 策略下部分场景自动提交,但显式提交更可靠)。
+- `insertRow(row)` 插入空行后再 `setData()` 填充;`removeRow(row)` 删除行。
+- `isDirty()` 判断模型是否有未提交改动,`submitAll()` 一次性提交。
+- 开关按钮 `SwitchButton` 发射 `toggled(bool)` 信号,主线程用 `sender()` 判断是哪一个开关,从而更新对应行的 `flag` 字段,实现"界面状态 ↔ 数据库"双向同步。
+
+**运行效果**:程序启动显示闹钟列表,每条有开关;点击开关变灰/变亮并同步数据库;点「+」添加闹钟(弹窗选时分),点已有闹钟可修改或删除;重启程序后数据仍在(保存在 `alarm.db`)。
+
+### 4.5 数据库表格显示(18_sqlite_table)
+
+例程 `18_sqlite_table`:用 `QTableView` + `QSqlQueryModel` 显示员工表,用 `QDataWidgetMapper` 把选中行的字段映射到底部编辑控件,并读取数据库中的照片路径显示照片。
+
+**数据库初始化与建表:**
+
+```cpp
+#include <QHeaderView>
+#include <QSqlError>
+#include <QApplication>
+#include <QSqlRecord>
+
+/* 查看本机可用的数据库驱动 */
+QStringList drivers = QSqlDatabase::drivers();
+foreach (QString driver, drivers) {
+    qDebug() << driver;
+}
+
+/* 以 QSQLITE 驱动方式打开或者创建数据库 */
+sqlDatabase = QSqlDatabase::addDatabase("QSQLITE");
+sqlDatabase.setDatabaseName("employee.db");
+/* 以 open 的方式打开 employee.db 数据库,则会创建一个 employee.db */
+if (!sqlDatabase.open())
+    qDebug() << "连接数据库错误" << sqlDatabase.lastError() << endl;
+else
+    qDebug() << "连接数据库成功" << endl;
+
+QSqlQuery query(sqlDatabase);
+/* 使用指令式创建表 */
+query.exec("create table employee (id int primary key, name vchar(10), "
+           "age int, sex vchar(3), photo text)");
+
+QStringList photoPath;
+/* 当前可执行程序的路径 */
+QString path(QApplication::applicationDirPath());
+photoPath << path + "/photos/啊万.jpg" << path + "/photos/啊棠.jpg";
+
+/* 以指令的方式插入数据,如果数据已经存在则不会成功不能插入 */
+query.exec(tr("insert into employee values(1, '啊万', 27, '男', '%1')")
+           .arg(photoPath[0]));
+query.exec(tr("insert into employee values(2, '啊棠', 28, '女', '%1')")
+           .arg(photoPath[1]));
+```
+
+**只读查询模型 + QTableView:**
+
+```cpp
+/* QSqlQueryModel 适合用于查询数据,不能修改数据 */
+sqlQueryModel = new QSqlQueryModel(this);
+
+/* 选择编号、姓名、年龄和性别的内容显示到 tableView 上,
+ * 图片最后通过数据选择再读取到 Label 上 */
+sqlQueryModel->setQuery("select id, name, age, sex from employee");
+
+if (sqlQueryModel->lastError().isValid())
+    qDebug() << "选择数据失败!" << endl;
+
+sqlQueryModel->setHeaderData(0, Qt::Horizontal, "编号");
+sqlQueryModel->setHeaderData(1, Qt::Horizontal, "姓名");
+sqlQueryModel->setHeaderData(2, Qt::Horizontal, "年龄");
+sqlQueryModel->setHeaderData(3, Qt::Horizontal, "性别");
+
+tableView = new QTableView();
+tableView->setModel(sqlQueryModel);
+
+/* 设置显示平均分列 */
+tableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
+```
+
+**选中行 → 读取照片:**
+
+```cpp
+void MainWindow::on_currentRowChanged(const QModelIndex &current,
+                                      const QModelIndex &previous)
+{
+    Q_UNUSED(previous)
+    /* 更新数据映射行号,初始化时映射到第 0 行 */
+    dataWidgetMapper->setCurrentModelIndex(current);
+
+    /* 获取当前行号 */
+    int row = itemSelectionModel->currentIndex().row();
+    /* 获取当前模型记录 */
+    QSqlRecord record = sqlQueryModel->record(row);
+    /* 获取 id 信息 */
+    int id = record.value("id").toInt();
+
+    QSqlQuery query;
+    /* 使用 bindValue 绑定 prepare 里语句的值,需要使用 ":","是占位符" */
+    query.prepare("select photo from employee where id = :ID");
+    query.bindValue(":ID", id);
+    query.exec();
+    /* 返回到选择的第一条记录,因为 id 是唯一的,也只有一条记录 */
+    query.first();
+
+    /* 获取字段为 photo 的值,也就是存储照片的路径 */
+    QVariant temp = query.value("photo");
+    if (!temp.isValid()) {
+        qDebug() << "数据无效!" << endl;
+        return;
+    }
+
+    imageLabel->clear();
+    QImage image(temp.toString());
+    if (image.isNull()) {
+        qDebug() << "未找到" << temp.toString() << endl;
+        return;
+    }
+    /* 显示照片 */
+    imageLabel->setPixmap(QPixmap::fromImage(image));
+}
+```
+
+**数据映射(把模型字段映射到底部控件):**
+
+```cpp
+itemSelectionModel = new QItemSelectionModel(sqlQueryModel);
+tableView->setSelectionModel(itemSelectionModel);
+
+/* 信号槽连接,表示表中行数据变化时,触发槽函数 */
+connect(itemSelectionModel,
+        SIGNAL(currentRowChanged(QModelIndex, QModelIndex)),
+        this,
+        SLOT(on_currentRowChanged(QModelIndex, QModelIndex)));
+
+dataWidgetMapper = new QDataWidgetMapper(this);
+/* 设置为自动提交 */
+dataWidgetMapper->setSubmitPolicy(QDataWidgetMapper::AutoSubmit);
+dataWidgetMapper->setModel(sqlQueryModel);
+/* 创建数据映射,将前面的数据库内容映射到控件上 */
+dataWidgetMapper->addMapping(lineEdit, 1);     /* 姓名 → 单行输入框 */
+dataWidgetMapper->addMapping(spinBox[0], 0);   /* 编号 → 数值框 */
+dataWidgetMapper->addMapping(spinBox[1], 2);   /* 年龄 → 数值框 */
+dataWidgetMapper->addMapping(comboBox, 3);     /* 性别 → 下拉框 */
+```
+
+**另一种写法:可编辑的 QSqlTableModel(注释中给出):**
+
+```cpp
+QSqlTableModel *model = new QSqlTableModel(this, sqlDatabase);
+model->setTable("employee");
+model->setEditStrategy(QSqlTableModel::OnFieldChange);
+model->select();
+/* 设置表格的头信息,若不设置则显示数据库里的英文字段头信息 */
+model->setHeaderData(model->fieldIndex("id"), Qt::Horizontal, tr("编号"));
+model->setHeaderData(model->fieldIndex("name"), Qt::Horizontal, tr("姓名"));
+model->setHeaderData(model->fieldIndex("age"), Qt::Horizontal, tr("年龄"));
+model->setHeaderData(model->fieldIndex("sex"), Qt::Horizontal, tr("性别"));
+
+QTableView *view = new QTableView;
+view->setModel(model);
+view->hideColumn(4);       /* 不显示图片路径信息列 */
+setCentralWidget(view);
+```
+
+**逐段解释**:
+
+- `QSqlQueryModel` 与 `QSqlTableModel` 的区别:前者适合**只读查询**,`setQuery()` 传入任意 SQL;后者针对**单表**且可编辑,适合 `QTableView` 直接改数据。
+- `setHeaderData()` 设置表头显示的中文名,不设置则显示数据库英文字段。
+- `QItemSelectionModel` 发出 `currentRowChanged` 信号,实现"选中表格某行 → 更新下方详情"。
+- `QDataWidgetMapper` 把模型的列映射到具体控件(输入框、数值框、下拉框),随选中行自动更新控件内容;`AutoSubmit` 表示控件修改自动提交回模型。
+- `QSqlQuery::prepare()` + `bindValue(":ID", id)` 使用带占位符的预处理语句,避免 SQL 注入,也便于复用。
+- 照片路径存在数据库的 `photo` 文本字段中,查询出来后用 `QImage`/`QPixmap` 加载显示。
+
+**运行效果**:窗口上方是表格(只显示编号/姓名/年龄/性别),选中一行后下方对应控件显示该行数据,右侧显示该员工的照片;编辑图片路径外的字段会更新到模型。
+
+---
+
+## 5. 构建与运行
+
+### 5.1 各例程所需模块
+
+| 例程 | `.pro` 额外模块 |
+| ---- | --------------- |
+| 05、06 多线程 | 无(`core gui` 即可) |
+| 07、08、09、10、11、12 网络 | `QT += network` |
+| 13 按钮音效 | `QT += multimedia` |
+| 14 音乐播放器 | `QT += multimedia` |
+| 15 视频播放器 | `QT += multimedia multimediawidgets` |
+| 16 录音 | `QT += multimedia` |
+| 17、18 数据库 | `QT += core gui sql` |
+
+### 5.2 Ubuntu 上编译运行
+
+```bash
+# 用 Qt Creator 打开对应 .pro,选择 Desktop Qt 套件运行;
+# 或命令行:
+qmake 14_musicplayer.pro
+make -j4
+./14_musicplayer
+```
+
+前置条件:
+
+- 网络例程可直接运行(无需额外依赖);
+- 多媒体例程需先安装 GStreamer 解码插件(见 3.1 节);
+- 数据库例程使用 `QSQLITE` 驱动,Qt 自带,无需额外安装;
+- 音乐/视频/录音例程需要在可执行文件所在目录准备 `myMusic` / `myVideo` / `Sounds` 目录(录音会自动创建)。
+
+### 5.3 I.MX6ULL 开发板上运行
+
+1. 用带对应模块的交叉编译 Qt 套件编译。嵌入式 Qt 通常已裁剪,需确认包含 `network`、`multimedia`(含 `multimediawidgets`)、`sql` 及其插件(`sqldrivers/libqsqlite.so`、多媒体后端插件);
+2. 把可执行文件及依赖资源(音视频文件目录、图片资源)拷贝到开发板;
+3. 在开发板 Qt 运行环境下执行。
+
+要点:
+
+- **SQLite 驱动插件**:开发板上的 Qt 必须带 `sqldrivers/libqsqlite.so`,否则 `QSqlDatabase::addDatabase("QSQLITE")` 会失败;可用 `QSqlDatabase::drivers()` 打印确认;
+- **多媒体后端**:嵌入式上通常用 GStreamer 或 Qt 自带的音视频插件,播放失败时先检查插件与解码器;
+- **网络程序**:开发板与 PC 需处于同一网段;组播/广播需网络设备支持。
+
+---
+
+## 6. 面试精选
+
+### 6.1 Qt 有哪两种多线程写法?区别是什么?
+
+一是**继承 `QThread` 并重写 `run()`**,只有 `run()` 在子线程执行,类中其他方法仍在主线程;二是**继承 `QObject`,用 `QObject::moveToThread()` 把整个对象移到线程**,该对象的所有槽函数都在子线程执行。Qt 4.8 之后官方推荐第二种,因为它更灵活:可以有 `doWork1()`、`doWork2()` 等多份耗时工作,且通过信号槽调用天然跨线程。继承 `QThread` 时只有 `run()` 属于新线程,容易误以为其它方法也在子线程而写错。
+
+### 6.2 为什么不能直接 `terminate()` 终止线程?怎样安全终止?
+
+`QThread::terminate()` 会在任意时刻强行结束线程,此时线程可能正持有锁、正在修改共享数据,会导致资源泄漏、死锁或数据不一致,因此官方不推荐。安全做法是:在 worker 中用一个 `bool` 标志位控制循环,主线程通过调用一个加锁的方法(如 `stopWork()`)把标志位置 `false`,循环检测到后 `break` 退出;随后再 `quit()` + `wait()` 让线程正常结束。本例用 `QMutexLocker` 保护标志位,保证两个线程访问互斥。
+
+### 6.3 `QMutexLocker` 相比手动 `lock()/unlock()` 有什么好处?
+
+`QMutexLocker` 是 RAII 封装:构造时加锁,析构(离开作用域)时自动解锁。即使中途 `return`、抛异常或提前 `break`,也能保证解锁,避免忘记 `unlock()` 造成的死锁。用法上把需要保护的代码放到一个 `{}` 作用域里即可;应尽量缩小持锁范围,不要在持锁期间做耗时操作(如本例 `QThread::sleep(2)` 放在锁外)。
+
+### 6.4 Qt 中 TCP 与 UDP 的用法有何区别?`read()`/`write()` 对应传统的什么函数?
+
+TCP 面向连接:服务端用 `QTcpServer::listen()` 监听,`newConnection()` 信号后在槽里 `nextPendingConnection()` 取得通信 socket;客户端用 `QTcpSocket::connectToHost()` 主动连接,连接成功后收发。UDP 无连接:用 `QUdpSocket::bind()` 绑定端口即可接收,`writeDatagram()` 发送,`readDatagram()` 接收并同时获得发送方地址端口;支持单播、广播(`QHostAddress::Broadcast`)和组播(`joinMulticastGroup()`)。Qt 把 socket 当作 I/O 流,用 `read()`/`write()` 收发数据,对应传统 C socket 的 `recv()`/`send()`。
+
+### 6.5 Qt SQL 中 `QSqlTableModel` 与 `QSqlQueryModel` 有何区别?如何把表显示到界面?
+
+`QSqlTableModel` 面向**单张表**,可读可写,配合 `QTableView` 可直接编辑并同步到数据库,常用 `setTable()`、`setEditStrategy()`、`select()`、`insertRow()`、`setData()`、`submit()`、`removeRow()`;`QSqlQueryModel` 面向**任意查询**,只读,用 `setQuery()` 执行 SQL、`record()` 取行数据。显示时用 `QTableView::setModel()` 设置模型即可,表头用 `setHeaderData()` 改成中文;若要把某行字段映射到独立控件,用 `QItemSelectionModel` 监听 `currentRowChanged`,再配合 `QDataWidgetMapper::addMapping()` 把列映射到输入框等控件。
+
+---
+
+## 延伸阅读
+
+- Linux TCP 编程(socket API 底层原理):[[Linux+C+C++技术体系梳理/2. Linux系统编程/15. TCP编程]]
+- 文本与绘图基础:[[05-Qt应用开发/06-文本绘图与图表]]
+- 嵌入式交叉编译与项目实战:[[05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战]]
+
+---
+
+**内容来源**:正点原子《I.MX6U嵌入式Qt开发指南V1.2》第十章 多线程(10.1 继承 QThread 的线程、10.2 继承 QObject 的线程)、第十一章 网络编程(11.1 获取本机的网络信息、11.2 TCP 通信、11.3 UDP 通信、11.4 网络下载实例)、第十二章 多媒体(12.1 Qt 多媒体简介、12.2 音效文件播放、12.3 音乐播放器、12.4 视频播放器、12.5 录音)、第十三章 数据库(13.1 Qt SQL 简介、13.2 应用实例);例程 `Qt/02/05_qthread_example1` ~ `18_sqlite_table` 共 14 个。

+ 3230 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战.md

@@ -0,0 +1,3230 @@
+---
+title: 嵌入式Qt交叉编译与项目实战
+tags: [嵌入式Linux, Qt, I.MX6U, IMX6ULL, ALPHA, 交叉编译, Qt Creator, qmake, make, linuxfb, eglfs, Qt控制LED, Qt控制BEEP, QFile, QSerialPort, 串口, CAN, QCanBus, SocketCAN, QCanBusFrame, QCamera, OpenCV, 摄像头, 蓝牙, QBluetoothServer, QBluetoothSocket, BlueZ, Rfcomm, USER-KEY, QKeyEvent, Key_VolumeDown, AP3216C, I2C, 杂项设备, ICM20608, SPI, 智能家居, ESP8266, 原子云, WebSocket, QWebSocket, 语音识别, 百度AI, ASR, 车牌识别, OCR, APP主界面, QScroller, QPropertyAnimation, 视频监控, V4L2, UDP广播, 项目实战]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第十四章 I.MX6U Qt开发 ~ 第二十八章 视频监控项目;配套例程 Embedded-Qt-Tutorial/Qt/03、Qt/04"
+---
+
+# 嵌入式Qt交叉编译与项目实战
+
+> 💡 **关联知识**:[[05-Qt应用开发/01-C++基础与面向对象]]、[[05-Qt应用开发/02-Qt入门与项目结构]]、[[05-Qt应用开发/03-信号与槽]]、[[05-Qt应用开发/04-Qt控件详解]]、[[05-Qt应用开发/05-布局容器与模型视图]]、[[05-Qt应用开发/06-文本绘图与图表]]、[[05-Qt应用开发/07-多线程网络多媒体与数据库]]、[[05-Qt应用开发/面试-Qt应用开发]];延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/1. Qt概述]]、[[实际项目开发文档/加解密客户端(Qt+LibreSSL)/1.项目源代码]]
+
+前面七篇都在讲 Qt 的通用知识:C++、信号与槽、控件、布局、绘图、多线程、网络。本篇把这些知识真正“落到板子上”——先搭建 I.MX6U 的交叉编译环境,再让 Qt 去操作开发板上的 LED、蜂鸣器、串口、CAN、摄像头、蓝牙、按键和各类传感器,最后完成五个综合项目。本篇所有代码均来自正点原子《I.MX6U嵌入式Qt开发指南》配套例程,运行环境为正点原子 I.MX6U ALPHA/MINI 开发板的**出厂系统**(驱动与应用必须配套,请勿随意更换内核或设备树)。
+
+## 学习地图
+
+```mermaid
+flowchart TD
+    A["Qt 应用开发知识<br/>(第 1~7 篇)"] --> B["第十四章:I.MX6U Qt 开发<br/>交叉编译环境"]
+    B --> C1["命令行编译"]
+    B --> C2["Qt Creator 交叉套件"]
+    C1 --> D["Qt 控制硬件<br/>第十五~二十三章"]
+    C2 --> D
+    D --> E1["LED / BEEP(gpio-leds)"]
+    D --> E2["Serial Port(QSerialPort)"]
+    D --> E3["CAN Bus(QCanBus)"]
+    D --> E4["Camera(OpenCV + V4L2)"]
+    D --> E5["USB Bluetooth(BlueZ)"]
+    D --> E6["USER-KEY(QKeyEvent)"]
+    D --> E7["AP3216C(I2C)/ ICM20608(SPI)"]
+    D --> F["综合项目<br/>第二十四~二十八章"]
+    F --> G1["智能家居物联网(原子云)"]
+    F --> G2["语音识别(百度 AI)"]
+    F --> G3["APP 主界面(滑动页面)"]
+    F --> G4["车牌识别(百度 OCR)"]
+    F --> G5["视频监控(UDP 广播)"]
+
+    classDef base fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    classDef hw fill:#dcfce7,stroke:#16a34a,color:#14532d
+    classDef proj fill:#fef9c3,stroke:#ca8a04,color:#713f12
+    class A,B,C1,C2 base
+    class D,E1,E2,E3,E4,E5,E6,E7 hw
+    class F,G1,G2,G3,G4,G5 proj
+```
+
+---
+
+# 第一部分:交叉编译环境搭建(第十四章)
+
+## 1. 两种交叉编译方式
+
+正点原子的出厂系统运行的是 ARM 架构的 Qt 程序,而我们在 Ubuntu 上编写、编译代码(x86-64 架构)。要让 PC 上生成的程序能在 ARM 开发板上运行,必须使用**交叉编译器**:在一种平台上编译出另一种平台可执行文件的工具链。
+
+《I.MX6U嵌入式Qt开发指南》第十四章明确指出,交叉编译 Qt 应用程序有两种方法:
+
+| 方式 | 说明 | 优点 | 缺点 |
+| ---- | ---- | ---- | ---- |
+| **命令行编译** | 直接用交叉工具链的 `qmake` + `make` 编译 | 简单、可控、适合脚本化 | 需要手动拷贝可执行文件到板子运行 |
+| **Qt Creator 交叉套件** | 在 Qt Creator 中配置 ARM 的 Kit(Qt Versions + 编译器) | 可远程连接开发板一键运行调试 | 环境变量、路径等配置不对时较难排查 |
+
+教程建议:**先在 Ubuntu 上运行仿真**(x86 套件),确认界面与逻辑正确后,再**交叉编译到开发板**查看实际效果。若 Qt Creator 远程运行结果不正确(例如缺少环境变量或路径),**直接拷贝可执行文件到开发板上运行**更稳妥。
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。第十四章本身只给出了“参考《I.MX6U 用户快速体验》4.6 节命令行编译”与“参考《出厂系统 Qt 交叉编译环境搭建》”的指引,下面补充通用的交叉编译工作流,便于初学者理解原理。
+
+## 2. 通用交叉编译工作流
+
+一个典型的嵌入式 Qt 交叉编译流程如下:
+
+```mermaid
+flowchart LR
+    A["Ubuntu 上的<br/>Qt 工程 .pro"] --> B["交叉 qmake<br/>生成 Makefile"]
+    B --> C["make<br/>调用 arm 交叉编译器"]
+    C --> D["生成 ARM 可执行文件"]
+    D --> E["scp / U 盘 / NFS<br/>拷贝到开发板"]
+    E --> F["开发板上运行<br/>./app -platform eglfs"]
+
+    classDef step fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    class A,B,C,D,E,F step
+```
+
+### 2.1 qmake
+
+`qmake` 是 Qt 的项目构建工具,它读取 `*.pro` 文件,根据当前使用的 Qt 套件生成平台相关的 `Makefile`。同一个工程在 PC 上用 Desktop 套件的 qmake 生成 x86 的 Makefile,用 ARM 套件的 qmake 就生成 ARM 的 Makefile。
+
+```bash
+# 交叉工具链中的 qmake(路径以实际安装为准)
+/opt/Qt5.12.9/Tools/QtCreator/bin/qtcreator.sh &   # 带环境变量启动 Qt Creator
+```
+
+工程文件 `.pro` 是最核心的配置,几个常用条目:
+
+```qmake
+QT       += core gui                # 使用的 Qt 模块
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+CONFIG   += c++11
+TARGET   = 01_led                   # 目标可执行文件名,默认取工程名
+SOURCES += main.cpp mainwindow.cpp
+HEADERS += mainwindow.h
+```
+
+### 2.2 make
+
+`qmake` 生成 `Makefile` 后,执行 `make` 完成真正的编译与链接,`make -j N` 可多核并行编译:
+
+```bash
+mkdir build && cd build
+/opt/Qt5.12.9/Tools/QtCreator/bin/qmake ..   # 或交叉工具链中的 qmake
+make -j8
+```
+
+### 2.3 命令行编译的整体步骤
+
+```bash
+# 1. 进入 Qt 工程目录
+cd 01_led
+
+# 2. 用交叉 qmake 生成 Makefile(先 source 交叉环境变量)
+/path/to/arm-qmake 01_led.pro
+
+# 3. 编译
+make -j8
+
+# 4. 拷贝到开发板(假设板子 IP 为 192.168.1.100)
+scp 01_led root@192.168.1.100:/home/root/
+
+# 5. 在开发板串口终端运行
+cd /home/root
+./01_led
+```
+
+## 3. 平台插件:-platform linuxfb / eglfs
+
+Qt 程序在不同平台上显示,依赖不同的**平台插件(platform plugin)**。在嵌入式 Linux 上最常用的是 `linuxfb` 与 `eglfs`,可通过 `-platform` 参数指定:
+
+```bash
+./01_led -platform linuxfb     # 使用 Linux Framebuffer 直接输出
+./01_led -platform eglfs       # 使用 EGL + OpenGL ES 全屏输出
+```
+
+| 平台插件 | 全称 | 依赖 | 适用场景 |
+| -------- | ---- | ---- | -------- |
+| `linuxfb` | Linux Framebuffer | `/dev/fb0`,无需 GPU | 没有 GPU、或只想在帧缓冲上直接显示 |
+| `eglfs` | EGL Full Screen | EGL/OpenGL ES 驱动、GPU | 有 GPU 加速(I.MX6ULL 出厂系统常用),性能好 |
+| `xcb` | X11 | X Server | 桌面 Linux(Ubuntu 仿真时) |
+
+> ⚠️ **来源说明**:本节不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。第十四章正文并未展开平台插件,此处补充 `-platform` 的工作原理与常见取值,方便调试“程序能跑但屏幕没反应”的问题。在 Ubuntu 上仿真时默认使用 `xcb`,在开发板上运行时 Qt 会依据 `QT_QPA_PLATFORM` 环境变量或 `-platform` 参数选择插件。
+
+## 4. 第一个可编译模板:main.cpp
+
+任何 Qt Widgets 程序的入口都遵循同一结构。以 LED 工程为例:
+
+```cpp
+#include "mainwindow.h"
+#include <QApplication>
+
+int main(int argc, char *argv[])
+{
+    QApplication a(argc, argv);   /* 每个 Qt Widgets 程序必须有且仅有一个 QApplication */
+    MainWindow w;
+    w.show();                     /* 显示主窗口 */
+    return a.exec();              /* 进入事件循环,处理用户与系统事件 */
+}
+```
+
+---
+
+# 第二部分:Qt 控制硬件(第十五 ~ 二十三章)
+
+> 说明:本部分所有例程均基于正点原子 I.MX6ULL ALPHA/MINI 开发板的出厂系统。出厂内核已把常用外设注册成标准设备节点,因此 Qt 应用无需自己写驱动,只要“打开文件、读写数据”即可。这正是 Linux“一切皆文件”思想的体现。
+
+## 5. Qt 控制 LED(第十五章)
+
+### 5.1 资源简介
+
+ALPHA 和 MINI 开发板板载各有一个 LED。出厂内核已把它注册为 **gpio-leds** 类型设备,应用层可直接操作其亮度节点。LED 的控制接口为:
+
+```text
+/sys/devices/platform/leds/leds/sys-led/brightness
+```
+
+向该节点写 `"1"` 点亮,写 `"0"` 熄灭。注意出厂系统默认把 `sys-led` 的触发方式设为**心跳(heartbeat)**,要想手动控制,需先把触发方式改为 `none`:
+
+```bash
+echo none > /sys/class/leds/sys-led/trigger
+```
+
+### 5.2 完整源码
+
+**01_led.pro**(模块:`core gui widgets`)
+
+```qmake
+QT       += core gui
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+CONFIG   += c++11
+SOURCES += main.cpp mainwindow.cpp
+HEADERS += mainwindow.h
+```
+
+**mainwindow.h**
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QPushButton>
+#include <QFile>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    /* 按钮 */
+    QPushButton *pushButton;
+
+    /* 文件,用于访问 LED 设备节点 */
+    QFile file;
+
+    /* 设置 LED 的状态 */
+    void setLedState();
+
+    /* 获取 LED 的状态 */
+    bool getLedState();
+
+private slots:
+    void pushButtonClicked();
+};
+#endif // MAINWINDOW_H
+```
+
+**mainwindow.cpp**
+
+```cpp
+#include "mainwindow.h"
+#include <QDebug>
+#include <QGuiApplication>
+#include <QScreen>
+#include <QRect>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 获取屏幕的分辨率,Qt 官方建议使用这种方法获取屏幕分辨率,
+     * 防止多屏设备导致对应不上。注意,这是获取整个桌面系统的分辨率
+     */
+    QList<QScreen *> list_screen = QGuiApplication::screens();
+
+    /* 如果是 ARM 平台,直接设置大小为屏幕的大小 */
+#if __arm__
+    /* 重设大小 */
+    this->resize(list_screen.at(0)->geometry().width(),
+                 list_screen.at(0)->geometry().height());
+    /* 默认是出厂系统的 LED 心跳触发方式,想要控制 LED,
+     * 需要改变 LED 的触发方式,改为 none,即无触发
+     */
+    system("echo none > /sys/class/leds/sys-led/trigger");
+#else
+    /* 否则设置主窗体大小为 800x480 */
+    this->resize(800, 480);
+#endif
+
+    pushButton = new QPushButton(this);
+
+    /* 居中显示 */
+    pushButton->setMinimumSize(200, 50);
+    pushButton->setGeometry((this->width()  - pushButton->width())  / 2,
+                            (this->height() - pushButton->height()) / 2,
+                            pushButton->width(),
+                            pushButton->height());
+
+    /* 开发板的 LED 控制接口 */
+    file.setFileName("/sys/devices/platform/leds/leds/sys-led/brightness");
+
+    if (!file.exists())
+        /* 设置按钮的初始化文本 */
+        pushButton->setText("未获取到 LED 设备!");
+
+    /* 获取 LED 的状态 */
+    getLedState();
+
+    /* 信号槽连接 */
+    connect(pushButton, SIGNAL(clicked()),
+            this, SLOT(pushButtonClicked()));
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::setLedState()
+{
+    /* 在设置 LED 状态时先读取,防止外界改变了 LED 状态 */
+    bool state = getLedState();
+
+    /* 如果文件不存在,则返回 */
+    if (!file.exists())
+        return;
+
+    if (!file.open(QIODevice::ReadWrite))
+        qDebug() << file.errorString();
+
+    QByteArray buf[2] = {"0", "1"};
+
+    /* 写 0 或 1 */
+    if (state)
+        file.write(buf[0]);
+    else
+        file.write(buf[1]);
+
+    /* 关闭文件 */
+    file.close();
+
+    /* 重新获取 LED 的状态 */
+    getLedState();
+}
+
+bool MainWindow::getLedState()
+{
+    /* 如果文件不存在,则返回 */
+    if (!file.exists())
+        return false;
+
+    if (!file.open(QIODevice::ReadWrite))
+        qDebug() << file.errorString();
+
+    QTextStream in(&file);
+
+    /* 读取文件所有数据 */
+    QString buf = in.readLine();
+
+    /* 打印出读出的值 */
+    qDebug() << "buf: " << buf << endl;
+    file.close();
+
+    if (buf == "1") {
+        pushButton->setText("LED 点亮");
+        return true;
+    } else {
+        pushButton->setText("LED 熄灭");
+        return false;
+    }
+}
+
+void MainWindow::pushButtonClicked()
+{
+    /* 设置 LED 的状态 */
+    setLedState();
+}
+```
+
+### 5.3 运行效果
+
+- **Ubuntu 仿真**:由于 Ubuntu 不是开发板,找不到 LED 设备节点,故按钮初始化时显示“未获取到 LED 设备!”。
+- **开发板运行**:交叉编译后拷贝到板上运行,点击按钮即可在“LED 点亮 / LED 熄灭”之间切换,LED 状态随之反转。
+
+> 编程要点:`setLedState()` 在写入前先读取当前状态,再取反写入。这种 get/set 分离、对外留接口的写法,是嵌入式控制类代码的基本素养——即使外部程序改过 LED,反转逻辑也不会错乱。
+
+## 6. Qt 控制 BEEP(第十六章)
+
+### 6.1 资源简介
+
+ALPHA 和 MINI 板载一个蜂鸣器(BEEP)。它直接接在一个 GPIO 上(并非 PWM,受管脚资源限制),出厂内核同样将其注册为 **gpio-leds** 类型设备,因此控制方法与 LED 完全一致:
+
+```text
+/sys/devices/platform/leds/leds/beep/brightness
+```
+
+### 6.2 完整源码
+
+**02_beep.pro**:模块同 `01_led`(`core gui widgets`)。
+
+**mainwindow.h**
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QPushButton>
+#include <QFile>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    QPushButton *pushButton;   /* 按钮 */
+    QFile file;                /* 文件 */
+
+    void setBeepState();       /* 设置 BEEP 的状态 */
+    bool getBeepState();       /* 获取 BEEP 的状态 */
+
+private slots:
+    void pushButtonClicked();  /* 槽函数 */
+};
+#endif // MAINWINDOW_H
+```
+
+**mainwindow.cpp**(与 `01_led` 例程结构完全一致,仅设备节点与提示文本不同)
+
+```cpp
+/* 与 01_led 的 mainwindow.cpp 相同,只需改动两处 */
+
+/* 1. 设备节点改为蜂鸣器 */
+file.setFileName("/sys/devices/platform/leds/leds/beep/brightness");
+
+/* 2. getBeepState() 中按钮文本改为 */
+pushButton->setText(buf == "1" ? "BEEP 开" : "BEEP 关");
+```
+
+### 6.3 运行效果
+
+- **Ubuntu**:读不到 BEEP 节点,初始化显示“未获取到 BEEP 设备!”。
+- **开发板**:点击按钮在“BEEP 开 / BEEP 关”之间切换,蜂鸣器响/停。
+
+> 归纳:LED 与 BEEP 都是 **gpio-leds 子系统**设备,Qt 侧统一用 `QFile` 读写 `brightness` 节点。掌握这两个例子,就掌握了“用 Qt 点灯”的标准套路——这也是一切的起点。
+
+## 7. Qt 串口编程 Serial Port(第十七章)
+
+### 7.1 资源简介
+
+I.MX6U 出厂系统默认配置了两路可用串口:
+
+| 串口 | 设备节点 | 说明 |
+| ---- | -------- | ---- |
+| UART1 | `/dev/ttymxc0` | 已作为**调试串口**使用,不可占用 |
+| UART3 | `/dev/ttymxc2` | 留给用户编程使用 |
+
+因此 Qt 程序只能对 `ttymxc2` 编程。默认参数:波特率 115200、数据位 8、无校验、停止位 1、无流控。
+
+### 7.2 完整源码
+
+**03_serialport.pro**(模块:`core gui serialport widgets`)
+
+```qmake
+# 添加串口模块支持
+QT       += core gui serialport
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+CONFIG   += c++11
+SOURCES += main.cpp mainwindow.cpp
+HEADERS += mainwindow.h
+```
+
+**mainwindow.h**
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QSerialPort>
+#include <QSerialPortInfo>
+#include <QPushButton>
+#include <QTextBrowser>
+#include <QTextEdit>
+#include <QVBoxLayout>
+#include <QLabel>
+#include <QComboBox>
+#include <QGridLayout>
+#include <QMessageBox>
+#include <QDebug>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    QSerialPort *serialPort;      /* 串口对象 */
+    QTextBrowser *textBrowser;    /* 用作接收数据 */
+    QTextEdit *textEdit;          /* 用作发送数据 */
+    QPushButton *pushButton[2];   /* 按钮:发送、打开串口 */
+    QComboBox *comboBox[5];       /* 串口号/波特率/数据位/校验位/停止位 */
+    QLabel *label[5];             /* 标签 */
+    QVBoxLayout *vboxLayout;      /* 垂直布局 */
+    QGridLayout *gridLayout;      /* 网格布局 */
+    QWidget *mainWidget;          /* 主布局容器 */
+    QWidget *funcWidget;          /* 功能区域容器 */
+
+    void layoutInit();            /* 布局初始化 */
+    void scanSerialPort();        /* 扫描系统可用串口 */
+    void baudRateItemInit();      /* 波特率项初始化 */
+    void dataBitsItemInit();      /* 数据位项初始化 */
+    void parityItemInit();        /* 校验位项初始化 */
+    void stopBitsItemInit();      /* 停止位项初始化 */
+
+private slots:
+    void sendPushButtonClicked();            /* 发送数据 */
+    void openSerialPortPushButtonClicked();  /* 打开/关闭串口 */
+    void serialPortReadyRead();              /* 读取串口数据 */
+};
+#endif // MAINWINDOW_H
+```
+
+**mainwindow.cpp**
+
+```cpp
+#include "mainwindow.h"
+#include <QDebug>
+#include <QGuiApplication>
+#include <QScreen>
+#include <QRect>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    layoutInit();          /* 布局初始化 */
+
+    scanSerialPort();      /* 扫描系统的串口 */
+
+    baudRateItemInit();    /* 波特率项初始化 */
+    dataBitsItemInit();    /* 数据位项初始化 */
+    parityItemInit();      /* 校验位项初始化 */
+    stopBitsItemInit();    /* 停止位项初始化 */
+}
+
+void MainWindow::layoutInit()
+{
+    QList<QScreen *> list_screen = QGuiApplication::screens();
+
+#if __arm__
+    this->resize(list_screen.at(0)->geometry().width(),
+                 list_screen.at(0)->geometry().height());
+#else
+    this->resize(800, 480);
+#endif
+
+    /* 初始化 */
+    serialPort   = new QSerialPort(this);
+    textBrowser  = new QTextBrowser();
+    textEdit     = new QTextEdit();
+    vboxLayout   = new QVBoxLayout();
+    funcWidget   = new QWidget();
+    mainWidget   = new QWidget();
+    gridLayout   = new QGridLayout();
+
+    /* QList 链表,字符串类型 */
+    QList<QString> list1;
+    list1 << "串口号:" << "波特率:" << "数据位:" << "校验位:" << "停止位:";
+
+    for (int i = 0; i < 5; i++) {
+        label[i] = new QLabel(list1[i]);
+        label[i]->setMinimumSize(80, 30);
+        label[i]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+        gridLayout->addWidget(label[i], 0, i);          /* 添加至网格坐标 (0, i) */
+    }
+
+    for (int i = 0; i < 5; i++) {
+        comboBox[i] = new QComboBox();
+        comboBox[i]->setMinimumSize(80, 30);
+        comboBox[i]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+        gridLayout->addWidget(comboBox[i], 1, i);       /* 添加至网格坐标 (1, i) */
+    }
+
+    QList<QString> list2;
+    list2 << "发送" << "打开串口";
+
+    for (int i = 0; i < 2; i++) {
+        pushButton[i] = new QPushButton(list2[i]);
+        pushButton[i]->setMinimumSize(80, 30);
+        pushButton[i]->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+        gridLayout->addWidget(pushButton[i], i, 5);     /* 添加至网格坐标 (i, 5) */
+    }
+    pushButton[0]->setEnabled(false);
+
+    /* 布局 */
+    vboxLayout->addWidget(textBrowser);
+    vboxLayout->addWidget(textEdit);
+    funcWidget->setLayout(gridLayout);
+    vboxLayout->addWidget(funcWidget);
+    mainWidget->setLayout(vboxLayout);
+    this->setCentralWidget(mainWidget);
+
+    /* 占位文本 */
+    textBrowser->setPlaceholderText("接收到的消息");
+    textEdit->setText("www.openedv.com");
+
+    /* 信号槽连接 */
+    connect(pushButton[0], SIGNAL(clicked()),
+            this, SLOT(sendPushButtonClicked()));
+    connect(pushButton[1], SIGNAL(clicked()),
+            this, SLOT(openSerialPortPushButtonClicked()));
+    connect(serialPort, SIGNAL(readyRead()),
+            this, SLOT(serialPortReadyRead()));
+}
+
+/* 查找可用串口并添加到 comboBox[0] */
+void MainWindow::scanSerialPort()
+{
+    foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
+        comboBox[0]->addItem(info.portName());
+    }
+}
+
+/* 波特率项初始化,默认 115200 */
+void MainWindow::baudRateItemInit()
+{
+    QList<QString> list;
+    list << "1200" << "2400" << "4800" << "9600"
+         << "19200" << "38400" << "57600"
+         << "115200" << "230400" << "460800"
+         << "921600";
+    for (int i = 0; i < 11; i++)
+        comboBox[1]->addItem(list[i]);
+    comboBox[1]->setCurrentIndex(7);   /* 115200 */
+}
+
+/* 数据位项初始化,默认 8 位 */
+void MainWindow::dataBitsItemInit()
+{
+    QList<QString> list;
+    list << "5" << "6" << "7" << "8";
+    for (int i = 0; i < 4; i++)
+        comboBox[2]->addItem(list[i]);
+    comboBox[2]->setCurrentIndex(3);   /* 8 */
+}
+
+/* 校验位项初始化,默认无校验 */
+void MainWindow::parityItemInit()
+{
+    QList<QString> list;
+    list << "None" << "Even" << "Odd" << "Space" << "Mark";
+    for (int i = 0; i < 5; i++)
+        comboBox[3]->addItem(list[i]);
+    comboBox[3]->setCurrentIndex(0);   /* None */
+}
+
+/* 停止位项初始化,默认 1 位 */
+void MainWindow::stopBitsItemInit()
+{
+    QList<QString> list;
+    list << "1" << "2";
+    for (int i = 0; i < 2; i++)
+        comboBox[4]->addItem(list[i]);
+    comboBox[4]->setCurrentIndex(0);   /* 1 */
+}
+
+/* 发送数据:读取 textEdit 内容并转为 utf8 字节流写出 */
+void MainWindow::sendPushButtonClicked()
+{
+    QByteArray data = textEdit->toPlainText().toUtf8();
+    serialPort->write(data);
+}
+
+/* 打开或关闭串口 */
+void MainWindow::openSerialPortPushButtonClicked()
+{
+    if (pushButton[1]->text() == "打开串口") {
+        /* 设置串口名 */
+        serialPort->setPortName(comboBox[0]->currentText());
+        /* 设置波特率 */
+        serialPort->setBaudRate(comboBox[1]->currentText().toInt());
+        /* 设置数据位数 */
+        switch (comboBox[2]->currentText().toInt()) {
+        case 5: serialPort->setDataBits(QSerialPort::Data5); break;
+        case 6: serialPort->setDataBits(QSerialPort::Data6); break;
+        case 7: serialPort->setDataBits(QSerialPort::Data7); break;
+        case 8: serialPort->setDataBits(QSerialPort::Data8); break;
+        default: break;
+        }
+        /* 设置奇偶校验 */
+        switch (comboBox[3]->currentIndex()) {
+        case 0: serialPort->setParity(QSerialPort::NoParity);    break;
+        case 1: serialPort->setParity(QSerialPort::EvenParity);  break;
+        case 2: serialPort->setParity(QSerialPort::OddParity);   break;
+        case 3: serialPort->setParity(QSerialPort::SpaceParity); break;
+        case 4: serialPort->setParity(QSerialPort::MarkParity);  break;
+        default: break;
+        }
+        /* 设置停止位 */
+        switch (comboBox[4]->currentText().toInt()) {
+        case 1: serialPort->setStopBits(QSerialPort::OneStop); break;
+        case 2: serialPort->setStopBits(QSerialPort::TwoStop); break;
+        default: break;
+        }
+        /* 设置流控制 */
+        serialPort->setFlowControl(QSerialPort::NoFlowControl);
+
+        if (!serialPort->open(QIODevice::ReadWrite)) {
+            QMessageBox::about(NULL, "错误",
+                               "串口无法打开!可能串口已经被占用!");
+        } else {
+            for (int i = 0; i < 5; i++)
+                comboBox[i]->setEnabled(false);
+            pushButton[1]->setText("关闭串口");
+            pushButton[0]->setEnabled(true);
+        }
+    } else {
+        serialPort->close();
+        for (int i = 0; i < 5; i++)
+            comboBox[i]->setEnabled(true);
+        pushButton[1]->setText("打开串口");
+        pushButton[0]->setEnabled(false);
+    }
+}
+
+/* 从接收缓冲区读取数据并显示 */
+void MainWindow::serialPortReadyRead()
+{
+    QByteArray buf = serialPort->readAll();
+    textBrowser->insertPlainText(QString(buf));
+}
+
+MainWindow::~MainWindow()
+{
+}
+```
+
+### 7.3 运行效果
+
+- **Ubuntu**:可用 USB 转串口模块与另一台设备互发消息测试界面。
+- **开发板**:把串口线接到 UART3(`ttymxc2`),用电脑上的正点原子 XCOM 上位机,参数设为 115200-8-N-1、无流控,点击“打开串口”即可收发。默认下拉选项必须选 `ttymxc2`,而不是被调试占用的 `ttymxc0`。
+
+> 归纳:Qt 串口编程的骨架是 **`QSerialPortInfo::availablePorts()` 扫描 → `QSerialPort` 配置并 `open()` → `readyRead()` 收 / `write()` 发**。这与裸机 Linux 串口编程(`termios`)目标是等价的,只是 Qt 封装得更直观。
+
+## 8. Qt CAN 编程 CAN Bus(第十八章)
+
+### 8.1 资源简介
+
+从 Qt5.8 开始提供了 CAN Bus 类,正点原子 I.MX6U 出厂系统的 Qt 版本为 **Qt5.12.9**,可直接使用。底板上预留一路 CAN 接口(I.MX6U 芯片最大支持两路),为**普通 CAN(非 FD CAN)**,最大比特率 1000 kBit/s。
+
+使用前必须先由系统初始化 CAN。要让 CAN 在总线关闭错误后 100ms 自动恢复,并以比特率 1000000 开启:
+
+```bash
+ip link set up can0 type can bitrate 1000000 restart-ms 100
+```
+
+> 注意:Windows 没有 CAN 设备(外接 USB CAN 模块需厂商固件),因此本例如在 Windows 上运行,可用接口列表为空。
+
+### 8.2 完整源码
+
+**04_socketcan.pro**(模块:`core gui serialbus widgets`)
+
+```qmake
+QT       += core gui serialbus
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+CONFIG   += c++11
+SOURCES += main.cpp mainwindow.cpp
+HEADERS += mainwindow.h
+```
+
+**mainwindow.h**
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QCanBusDevice>
+#include <QCanBus>
+#include <QPushButton>
+#include <QTextBrowser>
+#include <QLineEdit>
+#include <QVBoxLayout>
+#include <QLabel>
+#include <QComboBox>
+#include <QGridLayout>
+#include <QMessageBox>
+#include <QDebug>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    QCanBusDevice *canDevice;     /* CAN 设备 */
+    QTextBrowser *textBrowser;    /* 接收数据 */
+    QLineEdit *lineEdit;          /* 发送数据 */
+    QPushButton *pushButton[2];   /* 发送、连接 CAN */
+    QComboBox *comboBox[3];       /* 插件类型、可用接口、比特率 */
+    QLabel *label[4];             /* 标签 */
+    QVBoxLayout *vboxLayout;
+    QGridLayout *gridLayout;
+    QWidget *mainWidget;
+    QWidget *funcWidget;
+
+    void layoutInit();            /* 布局初始化 */
+    void pluginItemInit();        /* 插件类型项初始化 */
+    void bitrateItemInit();       /* 比特率项初始化 */
+
+private slots:
+    void sendFrame();                                        /* 发送消息 */
+    void receivedFrames();                                   /* 接收消息 */
+    void pluginChanged(int);                                 /* 插件发生改变 */
+    void canDeviceErrors(QCanBusDevice::CanBusError) const;  /* 处理 CAN 错误 */
+    void connectDevice();                                    /* 连接或断开 CAN */
+};
+#endif // MAINWINDOW_H
+```
+
+**mainwindow.cpp**(核心功能代码,界面布局部分与串口例程同构,完整代码见 `Qt/03/04_socketcan/mainwindow.cpp`)
+
+```cpp
+#include "mainwindow.h"
+#include <QGuiApplication>
+#include <QScreen>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    /* 使用系统指令初始化 CAN,默认为 1000000bits/s。
+     * 先关闭 CAN,才能以新的速率重新开启 */
+    system("ifconfig can0 down");
+    system("ip link set up can0 type can bitrate 1000000 restart-ms 100");
+
+    layoutInit();          /* 布局初始化 */
+
+    pluginItemInit();      /* 可用插件初始化 */
+
+    /* 可用接口项初始化 */
+    pluginChanged(comboBox[0]->currentIndex());
+
+    bitrateItemInit();     /* 比特率项初始化 */
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+/* 格式化接收到的帧标志 */
+static QString frameFlags(const QCanBusFrame &frame)
+{
+    QString result = QLatin1String(" --- ");
+
+    if (frame.hasBitrateSwitch())
+        result[1] = QLatin1Char('B');
+    if (frame.hasErrorStateIndicator())
+        result[2] = QLatin1Char('E');
+    if (frame.hasLocalEcho())
+        result[3] = QLatin1Char('L');
+
+    return result;
+}
+
+/* 发送消息:第一个字段为帧 ID(16 进制),其后 8 个字节为数据 */
+void MainWindow::sendFrame()
+{
+    if (!canDevice)
+        return;
+
+    QString str = lineEdit->text();
+    QByteArray data = 0;
+    QString strTemp = nullptr;
+    QStringList strlist = str.split(' ');
+    for (int i = 1; i < strlist.count(); i++)
+        strTemp = strTemp + strlist[i];
+
+    /* 将字符串内容转为 QByteArray */
+    data = QByteArray::fromHex(strTemp.toLatin1());
+
+    bool ok;
+    /* 以 16 进制读取第一个数据作为帧 ID */
+    int framId = strlist[0].toInt(&ok, 16);
+    QCanBusFrame frame = QCanBusFrame(framId, data);
+
+    /* 写入帧 */
+    canDevice->writeFrame(frame);
+}
+
+/* 接收消息 */
+void MainWindow::receivedFrames()
+{
+    if (!canDevice)
+        return;
+
+    while (canDevice->framesAvailable()) {
+        const QCanBusFrame frame = canDevice->readFrame();
+        QString view;
+        if (frame.frameType() == QCanBusFrame::ErrorFrame)
+            view = canDevice->interpretErrorFrame(frame);
+        else
+            view = frame.toString();
+
+        const QString time = QString::fromLatin1("%1.%2  ")
+                .arg(frame.timeStamp().seconds(), 10, 10, QLatin1Char(' '))
+                .arg(frame.timeStamp().microSeconds() / 100, 4, 10, QLatin1Char('0'));
+
+        const QString flags = frameFlags(frame);
+        textBrowser->insertPlainText(time + flags + view + "\n");
+    }
+}
+
+/* 从系统读取可用的插件并显示到 comboBox[0],默认选中 socketcan */
+void MainWindow::pluginItemInit()
+{
+    comboBox[0]->addItems(QCanBus::instance()->plugins());
+    for (int i = 0; i < QCanBus::instance()->plugins().count(); i++) {
+        if (QCanBus::instance()->plugins().at(i) == "socketcan")
+            comboBox[0]->setCurrentIndex(i);
+    }
+    connect(comboBox[0], SIGNAL(currentIndexChanged(int)),
+            this, SLOT(pluginChanged(int)));
+}
+
+/* 插件类型改变:需要同步刷新可用接口 */
+void MainWindow::pluginChanged(int)
+{
+    QList<QCanBusDeviceInfo> interfaces;
+    comboBox[1]->clear();
+    interfaces = QCanBus::instance()
+            ->availableDevices(comboBox[0]->currentText());
+    for (const QCanBusDeviceInfo &info : qAsConst(interfaces)) {
+        comboBox[1]->addItem(info.name());
+    }
+}
+
+/* 常用比特率初始化,默认 1000000 */
+void MainWindow::bitrateItemInit()
+{
+    const QList<int> rates = {
+        10000, 20000, 50000, 100000, 125000,
+        250000, 500000, 800000, 1000000
+    };
+
+    for (int rate : rates)
+        comboBox[2]->addItem(QString::number(rate), rate);
+
+    comboBox[2]->setCurrentIndex(8);
+}
+
+/* 连接或断开 CAN */
+void MainWindow::connectDevice()
+{
+    if (pushButton[1]->text() == "连接CAN") {
+        /* 注意:Qt 的 QCanBusDevice::BitRateKey 不能设置比特率,
+         * 系统 CAN 必须使用 ip 指令以指定比特率初始化后再操作 */
+        QString cmd1 = tr("ifconfig %1 down")
+                .arg(comboBox[1]->currentText());
+        QString cmd2 = tr("ip link set up %1 type can bitrate %2 restart-ms 100")
+                .arg(comboBox[1]->currentText())
+                .arg(comboBox[2]->currentText());
+        system(cmd1.toStdString().c_str());
+        system(cmd2.toStdString().c_str());
+
+        QString errorString;
+        canDevice = QCanBus::instance()->
+                createDevice(comboBox[0]->currentText(),
+                             comboBox[1]->currentText(),
+                             &errorString);
+
+        if (!canDevice) {
+            label[3]->setText(
+                        tr("Error creating device '%1', reason: '%2'")
+                        .arg(comboBox[0]->currentText()).arg(errorString));
+            return;
+        }
+
+        if (!canDevice->connectDevice()) {
+            label[3]->setText(tr("Connection error: %1")
+                              .arg(canDevice->errorString()));
+            delete canDevice;
+            canDevice = nullptr;
+            return;
+        }
+
+        connect(canDevice, SIGNAL(framesReceived()),
+                this, SLOT(receivedFrames()));
+        connect(canDevice,
+                SIGNAL(errorOccurred(QCanBusDevice::CanBusError)),
+                this,
+                SLOT(canDeviceErrors(QCanBusDevice::CanBusError)));
+
+        label[3]->setText(
+                    tr("插件类型为: %1, 已连接到 %2, 比特率为 %3 kBit/s")
+                    .arg(comboBox[0]->currentText())
+                    .arg(comboBox[1]->currentText())
+                    .arg(comboBox[2]->currentText().toInt() / 1000));
+        pushButton[1]->setText("断开CAN");
+        pushButton[0]->setEnabled(true);
+        comboBox[0]->setEnabled(false);
+        comboBox[1]->setEnabled(false);
+        comboBox[2]->setEnabled(false);
+    } else {
+        if (!canDevice)
+            return;
+
+        canDevice->disconnectDevice();
+        delete canDevice;
+        canDevice = nullptr;
+        pushButton[1]->setText("连接CAN");
+        pushButton[0]->setEnabled(false);
+        label[3]->setText(tr("未连接!"));
+        comboBox[0]->setEnabled(true);
+        comboBox[1]->setEnabled(true);
+        comboBox[2]->setEnabled(true);
+    }
+}
+
+/* 错误处理 */
+void MainWindow::canDeviceErrors(QCanBusDevice::CanBusError error) const
+{
+    switch (error) {
+    case QCanBusDevice::ReadError:
+    case QCanBusDevice::WriteError:
+    case QCanBusDevice::ConnectionError:
+    case QCanBusDevice::ConfigurationError:
+    case QCanBusDevice::UnknownError:
+        label[3]->setText(canDevice->errorString());
+        break;
+    default:
+        break;
+    }
+}
+```
+
+`layoutInit()` 负责摆放“插件类型 / 可用接口 / 比特率”三个下拉框与“发送 / 连接CAN”两个按钮,其结构与串口例程相同;其中 `lineEdit` 默认文本为 `123 aa 77 66 55 44 33 22 11`,即帧 ID `0x123` + 8 字节数据。
+
+### 8.3 运行效果
+
+- **Ubuntu**:没有 CAN 设备,可用接口处不可选。
+- **开发板**:交叉编译后运行,插件类型默认(也必须)为 `socketcan`,可用接口为 `can0`。与 CAN 仪器以相同比特率通信,即可收发。发送格式每个字节用空格隔开,如 `123 aa 77 66 55 44 33 22 11`。
+
+> 关键点:`QCanBusDevice::BitRateKey` **不能**设置比特率,必须先用 `ip link set up can0 type can bitrate ...` 让系统 CAN 起来;Linux 下唯一可用插件是 `socketcan`。
+
+## 9. Qt + OpenCV 摄像头 Camera(第十九章)
+
+### 9.1 资源简介
+
+底板上有一路 CSI 摄像头接口,支持正点原子 OV5640、OV2640、OV7725(不带 FIFO),同时也有 USB 接口可接 USB 免驱摄像头。例程同时兼容这两类摄像头。
+
+**为什么不直接用 QCamera?** QCamera 在某些驱动上读取 RGB 格式数据,而 OV5640/OV2640/OV7725 的驱动默认输出 YUYV,格式对不上会无法使用。此外 V4L2 直接把数据送到 `fb0` 显示会比“经 Qt 控件处理”更流畅。因此本例采用 **Qt + OpenCV** 调用摄像头。
+
+出厂系统内置 OpenCV 版本为 3.1.0;若要在 Ubuntu 上仿真,可自行安装 OpenCV 3.4.1。使用交叉编译工具链时其中已包含 OpenCV,无需再装。
+
+### 9.2 完整源码
+
+**05_opencv_camera.pro**(模块:`core gui widgets` + OpenCV)
+
+```qmake
+QT       += core gui
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+CONFIG   += c++11
+
+TARGET_ARCH = $${QT_ARCH}
+contains(TARGET_ARCH, arm){
+    # ARM 套件:交叉工具链自带 OpenCV
+    CONFIG += link_pkgconfig
+    PKGCONFIG += opencv
+    INCLUDEPATH += /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots/cortexa7hf-neon-poky-linux-gnueabi/usr/include
+} else {
+    # 桌面套件:链接 Ubuntu 本地安装的 OpenCV
+    LIBS += -L/usr/local/lib \
+            -lopencv_core \
+            -lopencv_highgui \
+            -lopencv_imgproc \
+            -lopencv_videoio \
+            -lopencv_imgcodecs
+    INCLUDEPATH += /usr/local/include
+}
+
+SOURCES += camera.cpp main.cpp mainwindow.cpp
+HEADERS += camera.h mainwindow.h
+```
+
+**camera.h**
+
+```cpp
+#ifndef CAMERA_H
+#define CAMERA_H
+
+#include <QImage>
+#include <QTimer>
+
+/* 使用命名空间 cv 下的 VideoCapture 与 Mat 类 */
+namespace cv {
+class VideoCapture;
+class Mat;
+}
+
+class Camera : public QObject
+{
+    Q_OBJECT
+public:
+    explicit Camera(QObject *parent = nullptr);
+    ~Camera();
+
+signals:
+    /* 有图片信号时发出,用于显示图像 */
+    void readyImage(const QImage&);
+
+public slots:
+    bool cameraProcess(bool);          /* 开启 / 关闭定时器 */
+    void selectCameraDevice(int);      /* 选择摄像头 */
+
+private slots:
+    void timerTimeOut();               /* 定时器到,发送图像数据 */
+
+private:
+    cv::VideoCapture *capture;         /* OpenCV 的 VideoCapture 对象 */
+    QTimer *timer;                     /* 定时器 */
+    QImage matToQImage(const cv::Mat&); /* 图像转换 */
+};
+
+#endif // CAMERA_H
+```
+
+**camera.cpp**
+
+```cpp
+#include "camera.h"
+#include "opencv2/core/core.hpp"
+#include "opencv2/highgui/highgui.hpp"
+#include <QImage>
+#include <QDebug>
+
+Camera::Camera(QObject *parent) : QObject(parent)
+{
+    capture = new cv::VideoCapture();
+    timer = new QTimer(this);
+    connect(timer, SIGNAL(timeout()), this, SLOT(timerTimeOut()));
+}
+
+Camera::~Camera()
+{
+    delete capture;
+    capture = NULL;
+}
+
+void Camera::selectCameraDevice(int index)
+{
+    /* 如果已有其它摄像头打开,先释放 */
+    if (capture->isOpened())
+        capture->release();
+
+    /* 打开摄像头设备 */
+    capture->open(index);
+}
+
+bool Camera::cameraProcess(bool bl)
+{
+    if (bl) {
+        /* 33ms 约等于 30 帧,即一秒最多显示 30 帧 */
+        timer->start(33);
+    } else {
+        timer->stop();
+    }
+    return capture->isOpened();
+}
+
+void Camera::timerTimeOut()
+{
+    if (!capture->isOpened()) {
+        timer->stop();
+        return;
+    }
+
+    static cv::Mat frame;
+    *capture >> frame;
+    if (frame.cols)
+        emit readyImage(matToQImage(frame));
+}
+
+QImage Camera::matToQImage(const cv::Mat &img)
+{
+    /* USB 摄像头和 OV5640 等都是 RGB 三通道,不考虑单/四通道摄像头 */
+    if (img.type() == CV_8UC3) {
+        const uchar *pimg = (const uchar*)img.data;
+        QImage qImage(pimg, img.cols, img.rows, img.step,
+                      QImage::Format_RGB888);
+        /* 不改变图像数据的前提下交换红蓝通道 */
+        return qImage.rgbSwapped();
+    }
+    return QImage();
+}
+```
+
+**mainwindow.h**
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QVBoxLayout>
+#include <QHBoxLayout>
+#include <QComboBox>
+#include <QPushButton>
+#include <QLabel>
+#include <QScrollArea>
+#include <QDebug>
+
+class Camera;
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    QWidget *mainWidget;        /* 主容器 */
+    QScrollArea *scrollArea;    /* 滚动区域,方便高分辨率 */
+    QLabel *displayLabel;       /* 显示采集到的图像 */
+    QHBoxLayout *hboxLayout;
+    QVBoxLayout *vboxLayout;
+    QWidget *rightWidget;       /* 右侧区域容器 */
+    QLabel *photoLabel;         /* 显示拍照的缩略图 */
+    QComboBox *comboBox;        /* 摄像头设备选择框 */
+    QPushButton *pushButton[2]; /* 拍照、开始 */
+    QImage saveImage;           /* 拍照保存的照片 */
+    Camera *camera;             /* 摄像头对象 */
+
+    void layoutInit();
+    void scanCameraDevice();
+
+private slots:
+    void showImage(const QImage&);   /* 显示图像 */
+    void setButtonText(bool);        /* 设置按钮文本 */
+    void saveImageToLocal();         /* 保存照片到本地 */
+};
+#endif // MAINWINDOW_H
+```
+
+**mainwindow.cpp**(核心槽函数;布局初始化见例程源码)
+
+```cpp
+#include "mainwindow.h"
+#include <QGuiApplication>
+#include <QScreen>
+#include <QFile>
+#include <QPixmap>
+#include <QBuffer>
+#include "camera.h"
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    layoutInit();          /* 布局初始化 */
+    scanCameraDevice();    /* 扫描摄像头 */
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+/* 扫描 Linux 下的 /dev/video* 节点 */
+void MainWindow::scanCameraDevice()
+{
+#if win32
+    comboBox->addItem("windows 摄像头0");
+    connect(comboBox, SIGNAL(currentIndexChanged(int)),
+            camera, SLOT(selectCameraDevice(int)));
+#else
+    QFile file("/dev/video0");
+    if (file.exists())
+        comboBox->addItem("video0");
+    else {
+        displayLabel->setText("无摄像头设备");
+        return;
+    }
+
+    file.setFileName("/dev/video1");
+    if (file.exists()) {
+        comboBox->addItem("video1");
+        /* 开发板 ov5640 等设备是 1 */
+        comboBox->setCurrentIndex(1);
+    }
+
+    file.setFileName("/dev/video2");
+    if (file.exists())
+        /* 开发板 USB 摄像头设备是 2 */
+        comboBox->addItem("video2");
+
+#if !__arm__
+    /* Ubuntu 的 USB 摄像头一般是 0 */
+    comboBox->setCurrentIndex(0);
+#endif
+
+    connect(comboBox, SIGNAL(currentIndexChanged(int)),
+            camera, SLOT(selectCameraDevice(int)));
+#endif
+}
+
+/* 显示图像并更新拍照按钮 */
+void MainWindow::showImage(const QImage &image)
+{
+    displayLabel->setPixmap(QPixmap::fromImage(image));
+    saveImage = image;
+
+    if (!saveImage.isNull())
+        pushButton[0]->setEnabled(true);
+    else
+        pushButton[0]->setEnabled(false);
+}
+
+/* 开始 / 关闭摄像头 */
+void MainWindow::setButtonText(bool bl)
+{
+    if (bl) {
+        camera->selectCameraDevice(comboBox->currentIndex());
+        pushButton[1]->setText("关闭");
+    } else {
+        pushButton[0]->setEnabled(false);
+        pushButton[1]->setText("开始");
+    }
+}
+
+/* 保存照片到可执行程序目录下的 test.png */
+void MainWindow::saveImageToLocal()
+{
+    if (!saveImage.isNull()) {
+        QString fileName =
+                QCoreApplication::applicationDirPath() + "/test.png";
+        qDebug() << "正在保存" << fileName << "图片,请稍候..." << endl;
+
+        /* save(路径, 类型, 质量等级) */
+        saveImage.save(fileName, "PNG", -1);
+
+        photoLabel->setPixmap(QPixmap::fromImage(QImage(fileName)));
+        qDebug() << "保存完成!" << endl;
+    }
+}
+```
+
+`layoutInit()` 中通过 5 个信号槽把界面与逻辑连起来:
+
+```cpp
+camera = new Camera(this);
+connect(camera,   SIGNAL(readyImage(QImage)), this, SLOT(showImage(QImage)));
+connect(pushButton[1], SIGNAL(clicked(bool)), camera, SLOT(cameraProcess(bool)));
+connect(pushButton[1], SIGNAL(clicked(bool)), this,   SLOT(setButtonText(bool)));
+connect(pushButton[0], SIGNAL(clicked()),     this,   SLOT(saveImageToLocal()));
+```
+
+### 9.3 运行效果
+
+- 选择摄像头设备(开发板上 `video0` 是 NXP 的 pxp 驱动节点,**不要选**;OV5640 为 `video1`,USB 摄像头为 `video2`),点击“开始”即可预览,点击“拍照”在程序目录生成 `test.png` 并显示缩略图。
+- OpenCV 不设置分辨率时默认 640×480、30fps。USB 免驱摄像头可用 `capture->set(CV_CAP_PROP_FRAME_WIDTH, 320)` 修改分辨率;正点原子 OV5640/OV2640/OV7725 的驱动设置分辨率方式与标准 V4L2 有差异,可直接用 `camera_settings` 工具设置。
+
+## 10. Qt 蓝牙聊天 USB Bluetooth(第二十章)
+
+### 10.1 资源简介
+
+开发板无板载蓝牙,但可外接**免驱 USB 蓝牙**模块。Qt 蓝牙依赖 Linux 的 **BlueZ** 协议栈,因此**Windows 无法使用**(需自行移植),Ubuntu 与 I.MX6U 出厂系统均可。
+
+例程把本地蓝牙做成**服务端**(手机作为客户端连接),同时也支持扫描并作为**客户端**连接他人,实现聊天。通信基于 **RFCOMM/SPP(串口端口)** 协议。
+
+### 10.2 项目结构与模块
+
+**06_bluetooth_chat.pro**(模块:`core gui bluetooth widgets`)
+
+```qmake
+QT       += core gui bluetooth
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+CONFIG   += c++11
+SOURCES += chatclient.cpp chatserver.cpp main.cpp mainwindow.cpp remoteselector.cpp
+HEADERS += chatclient.h chatserver.h mainwindow.h remoteselector.h
+```
+
+项目分为四大部分:
+
+| 文件 | 职责 |
+| ---- | ---- |
+| `chatserver.*` | 服务端:创建 RFCOMM 服务器、注册服务、收发消息 |
+| `chatclient.*` | 客户端:连接远端服务、收发消息 |
+| `remoteselector.*` | 远程选择器:扫描发现周围蓝牙服务 |
+| `mainwindow.*` | 主界面:Tab 页面、按钮逻辑,串联上述三类 |
+
+服务 UUID 定义为:`e8e10f95-1a70-4b27-9ccf-02010264e9c8`。
+
+### 10.3 完整源码
+
+**chatclient.h / chatclient.cpp(客户端,核心代码)**
+
+```cpp
+/* chatclient.h 关键声明 */
+class ChatClient : public QObject {
+    Q_OBJECT
+public:
+    void startClient(const QBluetoothServiceInfo &remoteService);
+    void stopClient();
+public slots:
+    void sendMessage(const QString &message);
+    void disconnect();
+signals:
+    void messageReceived(const QString &sender, const QString &message);
+    void connected(const QString &name);
+    void disconnected();
+private slots:
+    void readSocket();
+    void connected();
+private:
+    QBluetoothSocket *socket;
+};
+
+/* chatclient.cpp:用扫描到的服务信息建立 RFCOMM socket 并连接 */
+void ChatClient::startClient(const QBluetoothServiceInfo &remoteService)
+{
+    if (socket) return;
+    socket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol);
+    socket->connectToService(remoteService);
+    connect(socket, SIGNAL(readyRead()),    this, SLOT(readSocket()));
+    connect(socket, SIGNAL(connected()),    this, SLOT(connected()));
+    connect(socket, SIGNAL(disconnected()), this, SIGNAL(disconnected()));
+}
+
+void ChatClient::readSocket()
+{
+    while (socket->canReadLine()) {
+        QByteArray line = socket->readLine();
+        emit messageReceived(socket->peerName(),
+                             QString::fromUtf8(line.constData(), line.length()));
+    }
+}
+
+void ChatClient::sendMessage(const QString &message)
+{
+    socket->write(message.toUtf8() + '\n');
+}
+```
+
+**chatserver.h / chatserver.cpp(服务端,核心代码)**
+
+```cpp
+/* 创建 RFCOMM 服务器并注册 SPP 服务,最后 registerService 才生效 */
+void ChatServer::startServer(const QBluetoothAddress& localAdapter)
+{
+    if (rfcommServer) return;
+    rfcommServer = new QBluetoothServer(QBluetoothServiceInfo::RfcommProtocol, this);
+    connect(rfcommServer, SIGNAL(newConnection()), this, SLOT(clientConnected()));
+    if (!rfcommServer->listen(localAdapter)) return;      /* 监听失败直接返回 */
+
+    /* 声明为串行端口(SPP),并绑定自定义 serviceUuid */
+    QBluetoothServiceInfo::Sequence classId;
+    classId << QVariant::fromValue(QBluetoothUuid(QBluetoothUuid::SerialPort));
+    serviceInfo.setAttribute(QBluetoothServiceInfo::BluetoothProfileDescriptorList, classId);
+    classId.prepend(QVariant::fromValue(QBluetoothUuid(serviceUuid)));
+    serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, classId);
+    serviceInfo.setServiceUuid(QBluetoothUuid(serviceUuid));
+    /* 还需设置 ServiceName / ServiceDescription / ServiceProvider /
+     * BrowseGroupList / ProtocolDescriptorList(L2cap + Rfcomm),
+     * 完整代码见 Qt/03/06_bluetooth_chat/chatserver.cpp */
+
+    serviceInfo.registerService(localAdapter);            /* 必须注册才会生效 */
+}
+
+void ChatServer::clientConnected()
+{
+    QBluetoothSocket *socket = rfcommServer->nextPendingConnection();
+    if (!socket) return;
+    connect(socket, SIGNAL(readyRead()),     this, SLOT(readSocket()));
+    connect(socket, SIGNAL(disconnected()),  this, SLOT(clientDisconnected()));
+    clientSockets.append(socket);
+    socketsPeername.append(socket->peerName());
+    emit clientConnected(socket->peerName());
+}
+```
+
+**remoteselector.h / remoteselector.cpp(远程发现,核心代码)**
+
+```cpp
+/* 初始化本地蓝牙服务发现代理 */
+RemoteSelector::RemoteSelector(QBluetoothAddress &localAdapter, QObject *parent)
+    : QObject(parent)
+{
+    m_discoveryAgent = new QBluetoothServiceDiscoveryAgent(localAdapter);
+    connect(m_discoveryAgent, SIGNAL(serviceDiscovered(QBluetoothServiceInfo)),
+            this, SLOT(serviceDiscovered(QBluetoothServiceInfo)));
+    connect(m_discoveryAgent, SIGNAL(finished()), this, SLOT(discoveryFinished()));
+    connect(m_discoveryAgent, SIGNAL(canceled()), this, SLOT(discoveryFinished()));
+}
+
+/* 注意:不要设置 uuid 过滤,否则搜不到手机 */
+void RemoteSelector::startDiscovery(const QBluetoothUuid &uuid)
+{
+    Q_UNUSED(uuid)
+    if (m_discoveryAgent->isActive())
+        m_discoveryAgent->stop();
+    // m_discoveryAgent->setUuidFilter(uuid);
+    m_discoveryAgent->start(QBluetoothServiceDiscoveryAgent::FullDiscovery);
+}
+
+/* 发现一个服务,按设备地址去重后加入列表 */
+void RemoteSelector::serviceDiscovered(const QBluetoothServiceInfo &serviceInfo)
+{
+    QMapIterator<QString, QBluetoothServiceInfo> i(m_discoveredServices);
+    while (i.hasNext()) {
+        i.next();
+        if (serviceInfo.device().address() == i.value().device().address())
+            return;
+    }
+    QString remoteName = serviceInfo.device().name().isEmpty()
+            ? serviceInfo.device().address().toString()
+            : serviceInfo.device().name();
+    QListWidgetItem *item = new QListWidgetItem(
+                QString::fromLatin1("%1%2").arg(remoteName, serviceInfo.serviceName()));
+    m_discoveredServices.insert(remoteName, serviceInfo);
+    emit newServiceFound(item);
+}
+```
+
+**mainwindow.h / mainwindow.cpp(关键逻辑)**
+
+`mainwindow` 使用一个 `QTabWidget` 分成“蓝牙聊天”和“蓝牙列表”两页,前者是 `QTextBrowser` + `QLineEdit` + 发送按钮,后者是 `QListWidget` + 扫描/停止/连接/断开按钮。核心槽函数如下:
+
+```cpp
+/* 初始化本地蓝牙,同时作为服务端 */
+void MainWindow::localAdapterInit()
+{
+    localAdapters = QBluetoothLocalDevice::allDevices();
+
+    QBluetoothLocalDevice localDevice;
+    /* 让自己可被其他设备发现 */
+    localDevice.setHostMode(QBluetoothLocalDevice::HostDiscoverable);
+
+    QBluetoothAddress adapter = QBluetoothAddress();
+    remoteSelector = new RemoteSelector(adapter, this);
+    connect(remoteSelector, SIGNAL(newServiceFound(QListWidgetItem*)),
+            this, SLOT(newServiceFound(QListWidgetItem*)));
+
+    server = new ChatServer(this);
+    connect(server, SIGNAL(clientConnected(QString)),
+            this, SLOT(connected(QString)));
+    connect(server, SIGNAL(clientDisconnected(QString)),
+            this, SLOT(disconnected(QString)));
+    connect(server, SIGNAL(messageReceived(QString,QString)),
+            this, SLOT(showMessage(QString,QString)));
+    connect(this, SIGNAL(sendMessage(QString)),
+            server, SLOT(sendMessage(QString)));
+    connect(this, SIGNAL(disconnect()),
+            server, SLOT(disconnect()));
+
+    server->startServer();
+    localName = QBluetoothLocalDevice().name();
+}
+
+/* 本地蓝牙作为客户端连接远端服务 */
+void MainWindow::connectToDevice()
+{
+    if (listWidget->currentRow() == -1)
+        return;
+
+    QString name = listWidget->currentItem()->text();
+    QBluetoothServiceInfo service;
+    bool found = false;
+
+    QMapIterator<QString,QBluetoothServiceInfo>
+            i(remoteSelector->m_discoveredServices);
+    while (i.hasNext()) {
+        i.next();
+        if (i.key() == name) {
+            service = i.value();
+            found = true;
+            break;
+        }
+    }
+
+    if (found) {
+        ChatClient *client = new ChatClient(this);
+        connect(client, SIGNAL(messageReceived(QString,QString)),
+                this, SLOT(showMessage(QString,QString)));
+        connect(client, SIGNAL(disconnected()),
+                this, SLOT(clientDisconnected()));
+        connect(client, SIGNAL(connected(QString)),
+                this, SLOT(connected(QString)));
+        connect(this, SIGNAL(sendMessage(QString)),
+                client, SLOT(sendMessage(QString)));
+        connect(this, SIGNAL(disconnect()),
+                client, SLOT(disconnect()));
+
+        client->startClient(service);
+        clients.append(client);
+    }
+}
+
+/* 开始扫描周围蓝牙 */
+void MainWindow::searchForDevices()
+{
+    listWidget->clear();
+    if (remoteSelector) {
+        delete remoteSelector;
+        remoteSelector = NULL;
+    }
+
+    QBluetoothAddress adapter = QBluetoothAddress();
+    remoteSelector = new RemoteSelector(adapter, this);
+    connect(remoteSelector, SIGNAL(newServiceFound(QListWidgetItem*)),
+            this, SLOT(newServiceFound(QListWidgetItem*)));
+
+    remoteSelector->m_discoveredServices.clear();
+    remoteSelector->startDiscovery(QBluetoothUuid(serviceUuid));
+}
+
+/* 收到消息则显示,切到聊天页 */
+void MainWindow::showMessage(const QString &sender, const QString &message)
+{
+    textBrowser->insertPlainText(QString::fromLatin1("%1: %2\n")
+                                 .arg(sender, message));
+    tabWidget->setCurrentIndex(0);
+    textBrowser->moveCursor(QTextCursor::End);
+}
+
+/* 发送消息:先本地显示,再 emit 给服务端和所有客户端 */
+void MainWindow::sendMessage()
+{
+    showMessage(localName, lineEdit->text());
+    emit sendMessage(lineEdit->text());
+}
+```
+
+### 10.4 运行效果
+
+运行后默认开启服务端模式。手机安装蓝牙调试软件(安卓可用“蓝牙调试宝”“蓝牙串口助手”),配对后即可聊天。开发板上需先按《I.MX6U 用户快速体验》第 3.29 节开启蓝牙并允许被扫描,先配对再连接。若连接失败,常见原因是蓝牙质量、系统蓝牙软件未开启、或手机端调试软件不支持 SPP。Ubuntu 虚拟机上蓝牙反应可能较慢,开发板上更稳定。
+
+> 编程要点:Qt 蓝牙服务端必须 `registerService()`;`RemoteSelector` 中**不能**设置 UUID 过滤,否则手机扫不出来;扫描到的名字是“设备名 + 服务名”拼接,连接时用该名字去 `m_discoveredServices` 里取回完整的 `QBluetoothServiceInfo`。
+
+## 11. Qt 监测 USER-KEY(第二十一章)
+
+### 11.1 资源简介
+
+开发板板载一个按键 KEY0,出厂内核已将其注册为 **gpio-keys** 类型设备,键值为 **114**,对应的正是 Qt 的 `Qt::Key_VolumeDown`(音量减)。也就是说,KEY0 可以当作普通键盘的“音量减键”来用。在 Windows/Ubuntu 上测试时使用方向键“↓”(`Qt::Key_Down`)代替。
+
+### 11.2 完整源码
+
+**07_key.pro**(模块:`core gui widgets`)
+
+```qmake
+QT       += core gui
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+CONFIG   += c++11
+SOURCES += main.cpp mainwindow.cpp
+HEADERS += mainwindow.h
+```
+
+**mainwindow.h**
+
+```cpp
+#ifndef MAINWINDOW_H
+#define MAINWINDOW_H
+
+#include <QMainWindow>
+#include <QKeyEvent>
+#include <QLabel>
+#include <QDebug>
+#include <QEvent>
+
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    QLabel *label;                       /* 标签文本 */
+
+    void keyPressEvent(QKeyEvent *event);    /* 重写按键按下事件 */
+    void keyReleaseEvent(QKeyEvent *event);  /* 重写按键松开事件 */
+};
+
+#endif // MAINWINDOW_H
+```
+
+**mainwindow.cpp**
+
+```cpp
+#include "mainwindow.h"
+#include <QGuiApplication>
+#include <QScreen>
+#include <QRect>
+
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent)
+{
+    QList<QScreen *> list_screen = QGuiApplication::screens();
+
+#if __arm__
+    this->resize(list_screen.at(0)->geometry().width(),
+                 list_screen.at(0)->geometry().height());
+#else
+    this->setGeometry(0, 0, 800, 480);
+#endif
+
+    label = new QLabel(this);
+
+#if __arm__
+    label->setText("VolumeDown 松开状态");
+#else
+    label->setText("Down 按键松开");
+#endif
+
+    label->setAlignment(Qt::AlignCenter);
+    setCentralWidget(label);
+}
+
+MainWindow::~MainWindow()
+{
+}
+
+void MainWindow::keyPressEvent(QKeyEvent *event)
+{
+#if __arm__
+    /* 板子 KEY0 按键对应 VolumeDown */
+    if (event->key() == Qt::Key_VolumeDown)
+        label->setText("VolumeDown 按键按下");
+#else
+    if (event->key() == Qt::Key_Down)
+        label->setText("Down 按键按下");
+#endif
+    QWidget::keyPressEvent(event);   /* 保留默认事件处理 */
+}
+
+void MainWindow::keyReleaseEvent(QKeyEvent *event)
+{
+#if __arm__
+    if (event->key() == Qt::Key_VolumeDown)
+        label->setText("VolumeDown 按键松开");
+#else
+    if (event->key() == Qt::Key_Down)
+        label->setText("Down 按键松开");
+#endif
+    QWidget::keyReleaseEvent(event);
+}
+```
+
+### 11.3 运行效果
+
+- **Ubuntu/Windows**:窗口获得焦点时按“↓”显示“Down 按键按下”,松开显示“Down 按键松开”。
+- **开发板**:按 KEY0 显示“VolumeDown 按键按下”,松开显示“VolumeDown 按键松开”。
+
+> 归纳:只要设备被注册成标准的输入设备(gpio-keys),Qt 就能通过**重写 `keyPressEvent()` / `keyReleaseEvent()`** 捕获按键,无需自己写线程轮询 `/dev/input/eventX`。
+
+## 12. Qt 读取 AP3216C 三合一环境传感器(第二十二章)
+
+### 12.1 资源简介
+
+AP3216C 是**三合一环境传感器**(环境光 ALS + 接近 PS + 红外 IR),采用 **I2C** 接口,仅 ALPHA 底板有,MINI 没有。出厂系统已写好 AP3216C 驱动并注册为**杂项设备**,节点位于:
+
+```text
+/sys/class/misc/ap3216c/als
+/sys/class/misc/ap3216c/ps
+/sys/class/misc/ap3216c/ir
+```
+
+本例与 LED 的区别在于:读取方式改用 **C 语言的 `open()/read()/close()`** 而非 QFile,并且设计了一个蓝色科技感的弧形仪表界面,把界面拆成 `ArcGraph`(弧形图)、`GlowText`(发光文字)、`HeadView`(表头)等自定义类。分辨率关系:ALS 为 16 位(65535),PS/IR 为 10 位(1023)。
+
+### 12.2 完整源码
+
+**08_ii2c_ap3216c_sensor.pro**(模块:`core gui widgets`,含 `headview/headview.pri`)
+
+```qmake
+QT       += core gui
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
+CONFIG   += c++11
+SOURCES += ap3216c.cpp arcgraph.cpp glowtext.cpp main.cpp mainwindow.cpp
+HEADERS += ap3216c.h arcgraph.h glowtext.h mainwindow.h
+include(headview/headview.pri)
+```
+
+> `.pri` 文件语法与 `.pro` 相同,类似 C++ 的头文件,用 `include()` 引入,便于复用与管理大量项目文件。
+
+**ap3216c.h / ap3216c.cpp**(数据来源:C 语言接口读取 sysfs 节点)
+
+```cpp
+// ap3216c.h
+#ifndef AP3216C_H
+#define AP3216C_H
+
+#include <QObject>
+#include <QTimer>
+
+class Ap3216c : public QObject
+{
+    Q_OBJECT
+public:
+    explicit Ap3216c(QObject *parent = 0);
+    ~Ap3216c();
+
+    Q_INVOKABLE void setCapture(bool str);
+
+    QString alsData();
+    QString psData();
+    QString irData();
+
+    Q_PROPERTY(QString alsData READ alsData NOTIFY ap3216cDataChanged)
+    Q_PROPERTY(QString psData  READ psData  NOTIFY ap3216cDataChanged)
+    Q_PROPERTY(QString irData  READ irData  NOTIFY ap3216cDataChanged)
+
+private:
+    QTimer *timer;
+    QString alsdata, psdata, irdata;
+
+    QString readAlsData();
+    QString readPsData();
+    QString readIrData();
+
+public slots:
+    void timer_timeout();
+
+signals:
+    void ap3216cDataChanged();
+};
+
+#endif // AP3216C_H
+```
+
+```cpp
+// ap3216c.cpp
+#include "ap3216c.h"
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <QDebug>
+
+Ap3216c::Ap3216c(QObject *parent) : QObject(parent)
+{
+    timer = new QTimer();
+    connect(timer, SIGNAL(timeout()), this, SLOT(timer_timeout()));
+}
+
+Ap3216c::~Ap3216c()
+{
+}
+
+void Ap3216c::timer_timeout()
+{
+    alsdata = readAlsData();
+    psdata  = readPsData();
+    irdata  = readIrData();
+    emit ap3216cDataChanged();
+}
+
+QString Ap3216c::readIrData()
+{
+    char const *filename = "/sys/class/misc/ap3216c/ir";
+    int err = 0, fd;
+    char buf[10];
+
+    fd = open(filename, O_RDONLY);
+    if (fd < 0) {
+        close(fd);
+        return "open file error!";
+    }
+    err = read(fd, buf, sizeof(buf));
+    if (err < 0) {
+        close(fd);
+        return "read data error!";
+    }
+    close(fd);
+
+    QString irValue = buf;
+    QStringList list = irValue.split("\n");
+    return list[0];
+}
+
+QString Ap3216c::readPsData()
+{
+    char const *filename = "/sys/class/misc/ap3216c/ps";
+    int err = 0, fd;
+    char buf[10];
+
+    fd = open(filename, O_RDONLY);
+    if (fd < 0) { close(fd); return "open file error!"; }
+    err = read(fd, buf, sizeof(buf));
+    if (err < 0) { close(fd); return "read data error!"; }
+    close(fd);
+
+    QString psValue = buf;
+    return psValue.split("\n")[0];
+}
+
+QString Ap3216c::readAlsData()
+{
+    char const *filename = "/sys/class/misc/ap3216c/als";
+    int err = 0, fd;
+    char buf[10];
+
+    fd = open(filename, O_RDONLY);
+    if (fd < 0) { close(fd); return "open file error!"; }
+    err = read(fd, buf, sizeof(buf));
+    if (err < 0) { close(fd); return "read data error!"; }
+    close(fd);
+
+    QString alsValue = buf;
+    return alsValue.split("\n")[0];
+}
+
+QString Ap3216c::alsData() { return alsdata; }
+QString Ap3216c::irData()  { return irdata; }
+QString Ap3216c::psData()  { return psdata; }
+
+void Ap3216c::setCapture(bool str)
+{
+    if (str)
+        timer->start(500);   /* 默认 500ms 采集一次 */
+    else
+        timer->stop();
+}
+```
+
+**arcgraph.h / arcgraph.cpp**(蓝色科技感弧形视图,基于 QPainter)
+
+```cpp
+// arcgraph.h
+#ifndef ARCGRAPH_H
+#define ARCGRAPH_H
+
+#include <QWidget>
+#include <QPainter>
+#include <QPaintEvent>
+
+class ArcGraph : public QWidget
+{
+    Q_OBJECT
+public:
+    ArcGraph(QWidget *parent = nullptr);
+    ~ArcGraph();
+
+    void setstartAngle(int);
+    void setangleLength(int);
+
+private:
+    void paintEvent(QPaintEvent *event);
+    int startAngle;
+    int angleLength;
+};
+
+#endif // ARCGRAPH_H
+```
+
+```cpp
+// arcgraph.cpp(关键:QRadialGradient 画发光圆环,QPainterPath::arcTo 画弧)
+#include "arcgraph.h"
+
+ArcGraph::ArcGraph(QWidget *parent)
+    : QWidget(parent), startAngle(90), angleLength(100)
+{
+    this->setMinimumSize(100, 100);
+    setAttribute(Qt::WA_TranslucentBackground, true);
+}
+
+void ArcGraph::setstartAngle(int angle)   { startAngle = angle;   this->repaint(); }
+void ArcGraph::setangleLength(int length) { angleLength = length; this->repaint(); }
+
+void ArcGraph::paintEvent(QPaintEvent *event)
+{
+    QPainter painter(this);
+    painter.save();
+    painter.setRenderHints(QPainter::Antialiasing, true);
+
+    QRect drawRect = event->rect();
+
+    /* 两层径向渐变圆环:靠 setColorAt 在 0.5/0.51、0.42/0.43 处突变出环带 */
+    QRadialGradient g1(drawRect.center(), drawRect.width()/2, drawRect.center());
+    g1.setColorAt(0, Qt::transparent);
+    g1.setColorAt(0.51, QColor("#00237f"));
+    g1.setColorAt(0.59, Qt::transparent);
+    painter.setBrush(g1); painter.setPen(Qt::NoPen);
+    painter.drawEllipse(drawRect);
+
+    QRadialGradient g2(drawRect.center(), drawRect.width()/2, drawRect.center());
+    g2.setColorAt(0.421, QColor("#885881e3"));
+    g2.setColorAt(0.430, QColor("#5881e3"));
+    g2.setColorAt(0.441, Qt::transparent);
+    painter.setBrush(g2);
+    painter.drawEllipse(drawRect);
+
+    /* 中间显示百分比数字 */
+    QFont font; font.setPixelSize(drawRect.width()/10);
+    painter.setPen(Qt::white); painter.setFont(font);
+    painter.drawText(drawRect, Qt::AlignCenter,
+                     QString::number(angleLength * 100 / 360) + "%");
+
+    /* 平移到中心后 arcTo(矩形, 起始角, 扫过角),负值表示顺时针 */
+    painter.translate(drawRect.width() >> 1, drawRect.height() >> 1);
+    int radius = drawRect.width()/2;
+    QRectF rect(-radius, -radius, radius << 1, radius << 1);
+
+    QRadialGradient g3(0, 0, radius);
+    g3.setColorAt(0.51, QColor("#500194d3"));
+    g3.setColorAt(0.55, QColor("#22c1f3f9"));
+    g3.setColorAt(0.58, QColor("#500194d3"));
+    g3.setColorAt(0.68, Qt::transparent);
+    painter.setBrush(g3);
+    QPainterPath path1;
+    path1.arcTo(rect, startAngle, -angleLength);
+    painter.drawPath(path1);
+
+    QRadialGradient g4(0, 0, radius);
+    g4.setColorAt(0.50, QColor("#4bf3f9"));
+    g4.setColorAt(0.59, QColor("#4bf3f9"));
+    g4.setColorAt(0.60, Qt::transparent);
+    painter.setBrush(g4);
+    QPainterPath path2;
+    path2.arcTo(rect, startAngle, -angleLength);
+    painter.drawPath(path2);
+
+    painter.restore();
+    event->accept();
+}
+```
+
+**glowtext.h / glowtext.cpp**(发光文字:用 `QGraphicsBlurEffect` 做模糊发光)
+
+```cpp
+// glowtext.h
+#ifndef GLOWTEXT_H
+#define GLOWTEXT_H
+
+#include <QWidget>
+#include <QLabel>
+
+class GlowText : public QWidget
+{
+    Q_OBJECT
+public:
+    GlowText(QWidget *parent = nullptr);
+    ~GlowText();
+
+    void setTextColor(QColor);
+    void setFontSize(int);
+    void setTextData(QString);
+
+private:
+    QLabel *textLabelbg;   /* 文本背景(用于发光) */
+    QLabel *textLabel;     /* 文本标签 */
+    QColor textColor;
+    int fontSize;
+    QString textData;
+};
+
+#endif // GLOWTEXT_H
+```
+
+```cpp
+// glowtext.cpp(关键:同一段文字用两个 QLabel 叠加,底层加模糊特效做发光)
+#include "glowtext.h"
+#include <QGraphicsBlurEffect>
+
+GlowText::GlowText(QWidget *parent)
+    : QWidget(parent), textColor("#4bf3f9"), fontSize(18), textData("100")
+{
+    QFont font; font.setPixelSize(fontSize);
+    QPalette pal; pal.setColor(QPalette::WindowText, textColor);
+
+    /* 底层:模糊发光的文字 */
+    textLabelbg = new QLabel(this);
+    textLabelbg->setAttribute(Qt::WA_TranslucentBackground, true);
+    textLabelbg->setPalette(pal);
+    textLabelbg->setFont(font);
+    textLabelbg->setText(textData);
+    textLabelbg->setAlignment(Qt::AlignCenter);
+
+    QGraphicsBlurEffect *ef = new QGraphicsBlurEffect();
+    ef->setBlurRadius(25);
+    ef->setBlurHints(QGraphicsBlurEffect::QualityHint);
+    textLabelbg->setGraphicsEffect(ef);      /* 关键:模糊特效 */
+
+    /* 顶层:清晰文字 */
+    textLabel = new QLabel(this);
+    textLabel->setAttribute(Qt::WA_TranslucentBackground, true);
+    textLabel->setPalette(pal);
+    textLabel->setFont(font);
+    textLabel->setText(textData);
+    textLabel->setAlignment(Qt::AlignCenter);
+
+    textLabelbg->adjustSize();
+    textLabel->adjustSize();
+    this->resize(textLabel->size().width() + 10, textLabel->size().height() + 10);
+    this->setAttribute(Qt::WA_TranslucentBackground, true);
+}
+
+/* setTextColor / setFontSize / setTextData 均同时更新两层 QLabel 并 adjustSize(),
+ * 完整代码见 Qt/03/08_ii2c_ap3216c_sensor/glowtext.cpp */
+```
+
+**mainwindow.h / mainwindow.cpp**(把三个数据分别映射成弧形角度)
+
+```cpp
+// mainwindow.h(节选)
+class MainWindow : public QMainWindow
+{
+    Q_OBJECT
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+
+private:
+    ArcGraph *arcGraph[3];
+    GlowText *glowText[3];
+    QVBoxLayout *vBoxLayout;
+    QHBoxLayout *hBoxLayout[5];
+    QWidget *widget[6];
+    QLabel *label[3];
+    Ap3216c *ap3216c;
+    HeadView *headView;
+
+private slots:
+    void getAp3216cData();
+};
+```
+
+```cpp
+// mainwindow.cpp(核心部分)
+MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
+{
+    this->resize(800, 480);
+    this->setStyleSheet("background:#011753");
+
+    /* …… 大量布局代码(widget/arcGraph/label/glowText 的实例化与嵌套布局),
+     * 完整代码见 Qt/03/08_ii2c_ap3216c_sensor/mainwindow.cpp …… */
+
+    ap3216c = new Ap3216c(this);
+    /* 只有在开发板上才能获取数据,Ubuntu 上没有 ap3216c 传感器 */
+#if __arm__
+    ap3216c->setCapture(true);
+#endif
+
+    connect(ap3216c, SIGNAL(ap3216cDataChanged()),
+            this, SLOT(getAp3216cData()));
+}
+
+void MainWindow::getAp3216cData()
+{
+    /* 环境光 16 位,最大 65535 */
+    static QString als = ap3216c->alsData();
+    if (als != ap3216c->alsData()) {
+        als = ap3216c->alsData();
+        arcGraph[0]->setangleLength(als.toUInt() * 360 / 65535);
+    }
+
+    /* 接近距离 10 位,最大 1023 */
+    static QString ps = ap3216c->psData();
+    if (ps != ap3216c->psData()) {
+        ps = ap3216c->psData();
+        arcGraph[1]->setangleLength(ps.toUInt() * 360 / 1023);
+    }
+
+    /* 红外强度 10 位,最大 1023 */
+    static QString ir = ap3216c->irData();
+    if (ir != ap3216c->irData()) {
+        ir = ap3216c->irData();
+        arcGraph[2]->setangleLength(ir.toUInt() * 360 / 1023);
+    }
+
+    glowText[0]->setTextData(als);
+    glowText[1]->setTextData(ps);
+    glowText[2]->setTextData(ir);
+}
+```
+
+`HeadView`(数据可视化表头)是一个继承 `QWidget` 的透明组件,内部用 `QLabel` 显示图标与“数据可视化情况”文字,底部加一条 2px 的分隔线,可通过 `.pri` 整体移植到其他项目。
+
+### 12.3 运行效果
+
+- **Ubuntu**:显示初始化数据(读不到节点)。
+- **开发板 ALPHA**:手接近传感器时,三个弧形图的弧长与发光数字实时变化,默认每 500ms 采集一次。
+
+## 13. Qt 读取 ICM20608 六轴传感器(第二十三章)
+
+### 13.1 资源简介
+
+ICM20608 是**六轴 MEMS 传感器**(三轴加速度 + 三轴陀螺仪),采用 **SPI** 接口,仅 ALPHA 底板有。出厂系统提供了设备节点 `/dev/icm20608`。本章与上一章类似,只提供读取接口,不再重复设计界面。原理可参考《I.MX6U 嵌入式 Linux 驱动开发指南》Linux SPI 驱动实验。
+
+### 13.2 完整源码
+
+**icm20608.h / icm20608.cpp**
+
+```cpp
+// icm20608.h
+#ifndef ICM20608_H
+#define ICM20608_H
+
+#include <QObject>
+#include <QTimer>
+
+class Icm20608 : public QObject
+{
+    Q_OBJECT
+public:
+    explicit Icm20608(QObject *parent = 0);
+    ~Icm20608();
+
+    Q_INVOKABLE void setCapture(bool str);
+
+    Q_PROPERTY(QString gxData   READ gxData   NOTIFY icm20608DataChanged)
+    Q_PROPERTY(QString gyData   READ gyData   NOTIFY icm20608DataChanged)
+    Q_PROPERTY(QString gzData   READ gzData   NOTIFY icm20608DataChanged)
+    Q_PROPERTY(QString axData   READ axData   NOTIFY icm20608DataChanged)
+    Q_PROPERTY(QString ayData   READ ayData   NOTIFY icm20608DataChanged)
+    Q_PROPERTY(QString azData   READ azData   NOTIFY icm20608DataChanged)
+    Q_PROPERTY(QString tempData READ tempData NOTIFY icm20608DataChanged)
+
+private:
+    QTimer *timer;
+    QString gxdata, gydata, gzdata;
+    QString axdata, aydata, azdata;
+    QString tempdata;
+
+    QString gxData()   { return gxdata; }
+    QString gyData()   { return gydata; }
+    QString gzData()   { return gzdata; }
+    QString axData()   { return axdata; }
+    QString ayData()   { return aydata; }
+    QString azData()   { return azdata; }
+    QString tempData() { return tempdata; }
+
+    void icm20608ReadData();
+
+public slots:
+    void timer_timeout();
+
+signals:
+    void icm20608DataChanged();
+};
+
+#endif // ICM20608_H
+```
+
+```cpp
+// icm20608.cpp
+#include "icm20608.h"
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <QDebug>
+
+Icm20608::Icm20608(QObject *parent) : QObject(parent)
+{
+    timer = new QTimer();
+#if __arm__
+    /* 开发板上需要先加载 ICM20608 驱动模块 */
+    system("insmod /home/root/driver/icm20608/icm20608.ko");
+#endif
+    connect(timer, SIGNAL(timeout()), this, SLOT(timer_timeout()));
+}
+
+Icm20608::~Icm20608()
+{
+}
+
+void Icm20608::icm20608ReadData()
+{
+    int fd;
+    char const *filename = "/dev/icm20608";
+    signed int databuf[7];
+    unsigned char data[14];
+    signed int gyro_x_adc, gyro_y_adc, gyro_z_adc;
+    signed int accel_x_adc, accel_y_adc, accel_z_adc;
+    signed int temp_adc;
+
+    float gyro_x_act, gyro_y_act, gyro_z_act;
+    float accel_x_act, accel_y_act, accel_z_act;
+    float temp_act;
+
+    int ret = 0;
+
+    fd = open(filename, O_RDWR);
+    if (fd < 0) {
+        printf("can't open file %s\r\n", filename);
+        return;
+    }
+
+    ret = read(fd, databuf, sizeof(databuf));
+    /* 读取成功 */
+    if (ret == 0) {
+        gyro_x_adc = databuf[0];
+        gyro_y_adc = databuf[1];
+        gyro_z_adc = databuf[2];
+        accel_x_adc = databuf[3];
+        accel_y_adc = databuf[4];
+        accel_z_adc = databuf[5];
+        temp_adc    = databuf[6];
+
+        /* 按量程换算为实际值 */
+        gyro_x_act = (float)(gyro_x_adc) / 16.4;
+        gyro_y_act = (float)(gyro_y_adc) / 16.4;
+        gyro_z_act = (float)(gyro_z_adc) / 16.4;
+        accel_x_act = (float)(accel_x_adc) / 2048;
+        accel_y_act = (float)(accel_y_adc) / 2048;
+        accel_z_act = (float)(accel_z_adc) / 2048;
+        temp_act = ((float)(temp_adc) - 25) / 326.8 + 25;
+
+        gxdata   = QString::number(gyro_x_act, 'f', 2);
+        gydata   = QString::number(gyro_y_act, 'f', 2);
+        gzdata   = QString::number(gyro_z_act, 'f', 2);
+        axdata   = QString::number(accel_x_act, 'f', 2);
+        aydata   = QString::number(accel_y_act, 'f', 2);
+        azdata   = QString::number(accel_z_act, 'f', 2);
+        tempdata = QString::number(temp_act, 'f', 2);
+    }
+    close(fd);
+    emit icm20608DataChanged();
+}
+
+void Icm20608::timer_timeout()
+{
+    icm20608ReadData();
+}
+
+void Icm20608::setCapture(bool str)
+{
+    if (str)
+        timer->start(500);
+    else
+        timer->stop();
+}
+```
+
+### 13.3 运行效果
+
+在开发板 ALPHA 上加载驱动模块后,每 500ms 读取一次六轴数据与温度,并发出 `icm20608DataChanged()` 信号;界面部分可复用第二十二章的弧形/发光组件自行搭建。
+
+> 归纳:从第十五章到第二十三章,Qt 操作硬件的统一模式是——**确认设备节点 → 定时器 / 事件触发 → C 语言或 QFile 读写 → 通过信号把数据推送给界面**。区别只在于节点路径、数据格式与换算公式。
+
+---
+
+# 第三部分:综合项目实战(第二十四 ~ 二十八章)
+
+> 项目篇不再引入新语法,重点在**流程**:把设备端、云端、客户端串起来。以下每项给出流程图与核心源码;完整工程见 `Qt/04/`。
+
+## 14. 智能家居物联网项目(第二十四章)
+
+### 14.1 项目硬件与准备
+
+- 必备:正点原子 **ATK-ESP8266** 串口转 WIFI 模块(可免费接入原子云)、USB-TTL 模块、T 口 USB 线。PC 上可用串口调试助手直接测试模块。
+- 可选:可直接插在 ALPHA/Mini 底板的 ATK-MODULE 串口座上。
+- 注意:其他 WIFI 模块(如 Linux USB WIFI)或市售 WIFI 插座**不能**替代,因为它们需要刷特定固件才能接入对应云平台,无法二次开发。
+- 原子云上需要预先**分组**,并至少有一个命名为“客厅灯”的设备,记录其**设备编号**与**密码**(默认 `12345678`)。
+- Ubuntu 上访问 USB 模块需先放开权限:`sudo chmod 777 /dev/ttyUSB0`。
+
+### 14.2 系统流程
+
+```mermaid
+flowchart LR
+    subgraph Dev["设备端(开发板 / Ubuntu)"]
+        E["ESP8266 模块<br/>AT 指令"]
+        L["LED(gpio-leds)"]
+        E <-->|串口 ttymxc2 / ttyUSB0| L
+    end
+    subgraph Cloud["原子云"]
+        API["HTTPS API<br/>组织/分组/设备/状态"]
+        WS["WebSocket<br/>wss://cloud.alientek.com"]
+    end
+    subgraph App["控制端 UI(01_smarthome)"]
+        U["开关按钮"]
+    end
+    E <-->|WIFI| Cloud
+    U <-->|HTTPS + WebSocket| Cloud
+
+    classDef dev fill:#dcfce7,stroke:#16a34a,color:#14532d
+    classDef cloud fill:#fef9c3,stroke:#ca8a04,color:#713f12
+    classDef app fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    class E,L dev
+    class API,WS cloud
+    class U app
+```
+
+### 14.3 设备端:ESP8266 串口通信(`04/01_smarthome/esp8266/esp8266.cpp`)
+
+默认波特率 115200;开发板上串口用 `ttymxc2`,Ubuntu 上用 `ttyUSB0`。核心是通过 AT 指令依次完成:复位 → 设为 STA 模式 → 连 WIFI → 连原子云 → 定时发心跳 → 接收“开/关”控制 LED。
+
+```cpp
+Esp82266::Esp82266(QWidget *parent)
+{
+    Q_UNUSED(parent)
+    serialPort = new QSerialPort(this);   /* 与 ESP8266 通信的串口 */
+    timer      = new QTimer();            /* 定时发送在线心跳包 */
+    led        = new Led(this);           /* 收到云指令后控制板子 LED */
+
+#if __arm__
+    serialPort->setPortName("ttymxc2");
+#else
+    serialPort->setPortName("ttyUSB0");
+#endif
+    serialPort->setBaudRate(115200);
+    serialPort->setDataBits(QSerialPort::Data8);
+    serialPort->setParity(QSerialPort::NoParity);
+    serialPort->setStopBits(QSerialPort::OneStop);
+    serialPort->setFlowControl(QSerialPort::NoFlowControl);
+
+    if (!serialPort->open(QIODevice::ReadWrite))
+        qDebug() << "串口无法打开!可能正在被使用!" << endl;
+
+    connectToClound();                    /* 开始连接云 */
+    connect(serialPort, SIGNAL(readyRead()), this, SLOT(serialPortReadyRead()));
+    connect(timer, SIGNAL(timeout()), this, SLOT(onTimerTimeOut()));
+}
+
+/* 解析模块回显,按状态机推进 AT 流程 */
+void Esp82266::serialPortReadyRead()
+{
+    QByteArray buf = serialPort->readAll();
+    QString temp = QString(buf);
+    readData.append(temp);
+    qDebug() << temp << endl;
+
+    if (readData.contains("ready")) {                 /* 复位成功 */
+        sendCmdToEsp8266("AT+CWMODE=1");              /* 设为 STA 模式 */
+        readData.clear();
+    }
+    if (readData.contains("OK") && readData.contains("AT+CWMODE")) {
+        qDebug() << "设置STA 模式成功" << endl;
+        /* 请修改为自己的 WIFI 名称与密码 */
+        sendCmdToEsp8266("AT+CWJAP=\"ALIENTEK-YF\",\"15902020353\"");
+        readData.clear();
+    }
+    if (temp.contains("WIFI GOT IP")) {                /* 连上 WIFI */
+        sleep(2);
+        /* 原子云设备编号与密码,请改成自己的 */
+        sendCmdToEsp8266("AT+ATKCLDSTA=\"02314701717851074890\",\"12345678\"");
+    }
+    if (temp.contains("CLOUD CONNECTED")) {            /* 连上原子云 */
+        sleep(2);
+        timer->start(15000);                           /* 每 15s 发一次心跳 */
+    }
+    if (temp == "开")
+        led->setLedState(true);
+    else if (temp == "关")
+        led->setLedState(false);
+}
+
+void Esp82266::sendCmdToEsp8266(QString cmd)
+{
+    cmd = cmd + "\r\n";
+    serialPort->write(cmd.toUtf8());
+}
+
+void Esp82266::connectToClound()
+{
+    /* 复位模块(若已连过云,需断电或短接 RST 复位) */
+    sendCmdToEsp8266("AT+RST");
+}
+
+void Esp82266::onTimerTimeOut()
+{
+    sendTextMessage("online");        /* 心跳包 */
+    qDebug() << "发送设备在线心跳包" << endl;
+}
+```
+
+### 14.4 控制端:原子云 API + WebSocket(`04/01_smarthome/webapi/webapi.cpp`)
+
+原子云的**账号与设备信息走 HTTPS**,**实时通信用 WebSocket**。流程:用 `token` 请求组织列表 → 取 `org_id` → 请求分组列表 → 取 `group_id` → 请求设备列表 → 取 `device_id / number / name` → 请求设备连接状态 → 用 WebSocket 鉴权并订阅设备消息。
+
+```cpp
+Webapi::Webapi(QObject *parent)
+{
+    this->setParent(parent);
+    groupID.clear(); deviceID.clear(); deviceNumber.clear();
+
+    timer = new QTimer();
+    connect(timer, SIGNAL(timeout()), this, SLOT(onTimerTimeOut()));
+
+    networkAccessManager = new QNetworkAccessManager(this);
+
+    orgURL = "https://cloud.alientek.com/api/orgs";
+    api_token = "请填写自己的原子云 API TOKEN";   /* 原子云》账号信息 处查看 */
+
+    getOrgURL();                                  /* 获取账号机构列表 */
+
+    QUuid uuid = QUuid::createUuid();
+    random_token = uuid.toString();
+
+    webSocket = new QWebSocket();
+    /* 访问 https/wss 需要加安全配置 */
+    QSslConfiguration config;
+    config.setPeerVerifyMode(QSslSocket::VerifyNone);
+    config.setProtocol(QSsl::TlsV1SslV3);
+    webSocket->setSslConfiguration(config);
+
+    connect(webSocket, SIGNAL(connected()), this, SLOT(webSocketConnected()));
+    connect(webSocket, SIGNAL(binaryMessageReceived(QByteArray)),
+            this, SLOT(onBinaryMessageReceived(QByteArray)));
+}
+
+/* 从云服务器获取数据(HTTPS GET) */
+void Webapi::getDataFromWeb(QUrl url)
+{
+    QNetworkRequest networkRequest;
+    QSslConfiguration config;
+    config.setPeerVerifyMode(QSslSocket::VerifyNone);
+    config.setProtocol(QSsl::TlsV1SslV3);
+    networkRequest.setSslConfiguration(config);
+    networkRequest.setUrl(url);
+    networkRequest.setHeader(QNetworkRequest::ContentTypeHeader,
+                             "application/json;charset=UTF-8");
+    networkRequest.setRawHeader("token", api_token.toLatin1());  /* 账号 token */
+
+    QNetworkReply *newReply = networkAccessManager->get(networkRequest);
+    connect(newReply, SIGNAL(finished()),  this, SLOT(replyFinished()));
+    connect(newReply, SIGNAL(readyRead()), this, SLOT(readyReadData()));
+}
+
+/* 拿到 org_id 后拼出分组列表地址,同时用 token 打开 WebSocket 鉴权 */
+void Webapi::getID(QString data, QNetworkReply *reply)
+{
+    QRegularExpression pattern("\"id\":(\\d+)");
+    QRegularExpressionMatchIterator i = pattern.globalMatch(data);
+    while (i.hasNext()) {
+        QRegularExpressionMatch match = i.next();
+        if (match.hasMatch()) {
+            if (reply->url() == QUrl(orgURL)) {
+                org_id = match.captured(1);
+                groupListUrl = "https://cloud.alientek.com/api/orgs/"
+                        + org_id + "/grouplist";
+                getGroupListUrl();
+                webSocket->open(QUrl(QString(
+                    "wss://cloud.alientek.com/connection/%1/org/%2?token=%3")
+                    .arg(api_token).arg(org_id).arg(random_token)));
+            }
+            if (reply->url() == QUrl(groupListUrl)) {
+                groupID.append(match.captured(1));    /* 存组 ID */
+            }
+            if (reply->url() == QUrl(devOfGroupUrl)) {
+                deviceID.append(match.captured(1));   /* 存设备 ID */
+            }
+        }
+    }
+}
+
+/* 定时器:依次拉取“分组下设备 → 设备连接状态 → 订阅设备消息” */
+void Webapi::onTimerTimeOut()
+{
+    static int i = 0;
+    if (i < groupID.count()) {
+        devOfGroupUrl = "https://cloud.alientek.com/api/orgs/"
+                + org_id + "/groups/" + groupID[i] + "/devices";
+        dataString.clear();
+        getDevOfGroupUrl();
+    } else if (i >= groupID.count()
+               && i < groupID.count() + deviceID.count()) {
+        timer->start(1000);
+        conStateUrl = "https://cloud.alientek.com/api/orgs/"
+                + org_id + "/devicestate/" + deviceID[i - groupID.count()];
+        getConStateUrl();
+    } else {
+        /* 订阅所有设备的消息,命令字 0x01 */
+        for (int j = 0; j < deviceNumber.count(); j++) {
+            QByteArray cmd;
+            cmd[0] = 0x01;
+            sendCmd(deviceNumber[j], cmd);
+        }
+        timer->stop();
+    }
+    i++;
+}
+
+/* 向指定编号设备发送指令:命令字 0x03 + 消息内容(如“开”“关”) */
+void Webapi::whichDeviceNameSendCmd(QString name, QString message)
+{
+    for (int i = 0; i < deviceName.count(); i++) {
+        if (name == deviceName[i]) {
+            QByteArray cmd;
+            cmd[0] = 0x03;
+            sendCmdMessage(deviceNumber[i], cmd, message);
+            break;
+        }
+    }
+}
+```
+
+控制端 UI(`01_smarthome`)用 `QListWidget` + 自定义 `SwitchButton` 排列“客厅灯/台灯/空调”三个开关,切换时调用 `webapi->whichDeviceNameSendCmd(设备名, "开"/"关")`;收到 `deviceStateChanged` 信号后把对应标签改成“客厅灯|在线”并高亮显示。
+
+### 14.5 综合测试
+
+- **Ubuntu**:`esp8266.pro` 与 `01_smarthome.pro` 分别在两个 Qt Creator 中运行。设备端连上云后每 15s 发心跳;控制端订阅到“在线”后显示状态。点击开关即可远程控制,前提是 WIFI 账号密码、原子云 token、设备编号都改成自己的。
+- **开发板**:主控板先联网(网线 / USB WIFI / 4G / SDIO WIFI),可把 ESP8266 插在同一块板上,点击 UI 开关即经原子云转发控制板载 LED。
+
+## 15. 语音识别项目(第二十五章)
+
+### 15.1 流程与约束
+
+使用**百度语音识别**开放平台:注册账号、领取免费额度、创建“中文普通话”应用,得到 **API Key** 与 **Secret Key**。识别流程为:录一段 wav → 用 Key 换 `access_token` → 把音频 POST 到识别服务器 → 解析返回的 JSON 文本。
+
+硬性约束(百度短语音识别):采样率仅支持 **16000 / 8000**、**16bit 单声道**、时长 **≤ 60 秒**、格式支持 **wav/pcm**。正点原子出厂系统支持 wav 录制与播放,正好满足。注意 Linux C++ SDK 仅支持 x86-64,**不支持 ARM**,因此在开发板上要用 HTTP API,而非 SDK。
+
+```mermaid
+flowchart LR
+    A["点击界面图标"] --> B["播放提示音<br/>QSound"]
+    B --> C["延时 1.5s 后录音 8s<br/>QAudioRecorder → 16k.wav"]
+    C --> D["getTheResult()<br/>读取 wav 字节流"]
+    D --> E["HTTPS 换取 access_token"]
+    E --> F["POST audio/pcm;rate=16000<br/>到 vop.baidu.com/server_api"]
+    F --> G["解析 result 字段"]
+    G --> H{"含 开灯/关灯?"}
+    H -->|开灯| I["LED 点亮"]
+    H -->|关灯| J["LED 熄灭"]
+
+    classDef s fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    class A,B,C,D,E,F,G,H,I,J s
+```
+
+### 15.2 录音端:改为 16k 单声道 wav(`04/02_asr_demo/audiorecorder/audiorecorder.cpp`)
+
+```cpp
+void AudioRecorder::startRecorder()
+{
+    if (m_audioRecorder->state() == QMediaRecorder::StoppedState) {
+        m_audioRecorder->setAudioInput(devicesVar.at(0).toString());
+
+        QAudioEncoderSettings settings;
+        settings.setCodec(codecsVar.at(0).toString());
+        settings.setSampleRate(sampleRateVar[2].toInt());   /* 16000 采样率 */
+        settings.setBitRate(bitratesVar[0].toInt());
+        settings.setChannelCount(channelsVar[1].toInt());   /* 1 声道 */
+        settings.setQuality(QMultimedia::EncodingQuality(qualityVar[0].toInt()));
+        settings.setEncodingMode(QMultimedia::ConstantQualityEncoding);
+
+        /* I.MX6ULL 第 20 个支持的容器格式为 audio/x-wav */
+        QString container = containersVar.at(20).toString();
+
+        m_audioRecorder->setEncodingSettings(settings,
+                                             QVideoEncoderSettings(),
+                                             container);
+        /* 录音保存为当前可执行程序目录下的 16k.wav */
+        m_audioRecorder->setOutputLocation(QUrl::fromLocalFile(tr("./16k.wav")));
+        m_audioRecorder->record();
+    }
+}
+```
+
+### 15.3 识别端:换取 token 并发送音频(`04/02_asr_demo/asr/asr.cpp`)
+
+```cpp
+/* asr.h 中定义的关键地址 */
+// 换取 token 的接口
+const QString token_org =
+    "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials"
+    "&client_id=%1&client_secret=%2&";
+const QString api_key    = "填写自己的 API Key";
+const QString secret_key = "填写自己的 Secret Key";
+// 识别服务器接口,dev_pid=1537 表示普通话
+const QString server_api =
+    "http://vop.baidu.com/server_api?dev_pid=1537&cuid=%1&token=%2";
+
+/* 发起网络请求:token 走 GET,识别走 POST */
+void Asr::requestNetwork(QString url, QByteArray requestData)
+{
+    QNetworkRequest networkRequest;
+
+    QSslConfiguration config;
+    config.setPeerVerifyMode(QSslSocket::VerifyNone);
+    config.setProtocol(QSsl::TlsV1SslV3);
+    networkRequest.setSslConfiguration(config);
+
+    networkRequest.setHeader(QNetworkRequest::ContentTypeHeader,
+                             "application/json;charset=UTF-8");
+    /* 音频为 pcm、采样率 16000 */
+    networkRequest.setRawHeader("Content-Type",
+                                QString("audio/pcm;rate=16000").toLatin1());
+    networkRequest.setUrl(url);
+
+    QNetworkReply *newReply =
+            networkAccessManager->post(networkRequest, requestData);
+    connect(newReply, SIGNAL(finished()),  this, SLOT(replyFinished()));
+    connect(newReply, SIGNAL(readyRead()), this, SLOT(readyReadData()));
+}
+
+/* 读取响应:token 响应取 access_token;识别响应取 result */
+void Asr::readyReadData()
+{
+    QNetworkReply *reply = (QNetworkReply *)sender();
+    QByteArray data = reply->readAll();
+
+    if (reply->url() == QUrl(tokenUrl)) {
+        accessToken = getJsonValue(data, "access_token");
+        qDebug() << "获取token成功,可以调用getTheResult设置语音路径返回结果" << endl;
+    }
+    if (reply->url() == QUrl(serverApiUrl)) {
+        QString temp = getJsonValue(data, "result");
+        emit asrReadyData(temp);
+    }
+}
+
+/* 指定录音文件,读取字节流并 POST 到识别服务器 */
+void Asr::getTheResult(QString fileName)
+{
+    file.setFileName(fileName);
+    if (!file.exists()) { qDebug() << "文件" << fileName << "不存在" << endl; return; }
+
+    file.open(QIODevice::ReadOnly);
+    QByteArray requestData = file.readAll();
+    file.close();
+
+    serverApiUrl = QString(server_api)
+            .arg(QHostInfo::localHostName()).arg(accessToken);
+    requestNetwork(serverApiUrl, requestData);
+}
+```
+
+### 15.4 UI 与联动(`02_asr_demo/mainwindow.cpp`)
+
+```cpp
+/* 点击图标:播放音效,延时 1.5s 再录音,避免把提示音录进去 */
+bool MainWindow::eventFilter(QObject *watched, QEvent *event)
+{
+    if (watched == movieLabel && event->type() == QEvent::MouseButtonPress) {
+        QSound::play(":/audio/sound.wav");
+        if (myMovie->state() != QMovie::Running) {
+            timer2->start(1500);
+            textLabel->setText("正在听您说话,请继续...");
+            myMovie->start();
+        }
+    }
+    return QWidget::eventFilter(watched, event);
+}
+
+void MainWindow::onTimer2TimeOut()   /* 1.5s 后开始录音 */
+{
+    timer1->start(8000);             /* 录 8s 短语音 */
+    myAudioRecorder->startRecorder();
+    timer2->stop();
+}
+
+void MainWindow::onTimer1TimeOut()   /* 8s 到,停止录音并识别 */
+{
+    myAudioRecorder->stopRecorder();
+    textLabel->setText("正在识别,请稍候...");
+    timer1->stop();
+    myMovie->stop();
+    myAsr->getTheResult(QCoreApplication::applicationDirPath() + "/16k.wav");
+    timer3->start(30000);
+}
+
+/* 识别结果联动 LED */
+void MainWindow::onAsrReadyData(QString str)
+{
+    if (str.contains("开灯"))
+        myLed->setLedState(true);
+    else if (str.contains("关灯"))
+        myLed->setLedState(false);
+
+    textLabel->setText("识别结果是:\n" + str);
+    textLabel->adjustSize();
+}
+```
+
+运行前需在开发板运行 `/home/root/shell/audio/mic_in_config.sh` 打开麦头,并确保开发板联网。点击后等 1.5~2s 再说话,约 8s 后返回识别结果,准确率较高。
+
+## 16. APP 主界面开发项目(第二十六章)
+
+### 16.1 滑动页面(`slidepage`)
+
+Qt C++(非 QML)没有现成的手机式滑动页面。正点原子封装了一个 `SlidePage` 类:**`QScrollArea` + `QScroller` 手势 + `QPropertyAnimation` 动画**,通过 `addPage()` 添加页面,页面数 ≥ 2 即可滑动。原理是用水平滚动条的 `value` 控制当前页,松手后按滑动距离判断翻页方向并做动画。
+
+```cpp
+SlidePage::SlidePage(QWidget *parent)
+    : QWidget(parent), pageIndex(0), pageCount(0), draggingFlag(false)
+{
+    pageIndicator.clear();
+    this->setMinimumSize(400, 300);
+    this->setAttribute(Qt::WA_TranslucentBackground, true);
+
+    scrollArea = new QScrollArea(this);
+    scrollArea->setAlignment(Qt::AlignCenter);
+    mainWidget = new QWidget();
+    mainWidget->setStyleSheet("background: transparent");
+    scrollArea->setWidget(mainWidget);
+
+    /* 关闭滚动条显示 */
+    scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+    scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
+
+    /* 关键:用 QScroller 让 QScrollArea 支持手势滑动 */
+    scroller = QScroller::scroller(scrollArea);
+    QScroller::ScrollerGestureType gesture = QScroller::LeftMouseButtonGesture;
+    scroller->grabGesture(scrollArea, gesture);
+
+    QScrollerProperties properties = scroller->scrollerProperties();
+    properties.setScrollMetric(QScrollerProperties::SnapTime, 0.5);        /* 滑动时间 */
+    properties.setScrollMetric(QScrollerProperties::MinimumVelocity, 1);   /* 滑动速度 */
+    scroller->setScrollerProperties(properties);
+
+    hBoxLayout = new QHBoxLayout();
+    hBoxLayout->setContentsMargins(0, 0, 0, 0);
+    hBoxLayout->setSpacing(0);
+    mainWidget->setLayout(hBoxLayout);
+
+    /* 定时器:按下超过 300ms 视为拖动而非点击 */
+    timer = new QTimer(this);
+    connect(scrollArea->horizontalScrollBar(), SIGNAL(valueChanged(int)),
+            this, SLOT(hScrollBarValueChanged(int)));
+    connect(scroller, SIGNAL(stateChanged(QScroller::State)),
+            this, SLOT(onStateChanged(QScroller::State)));
+    connect(timer, SIGNAL(timeout()), this, SLOT(onTimerTimeOut()));
+    connect(this, SIGNAL(currentPageIndexChanged(int)),
+            this, SLOT(onCurrentPageIndexChanged(int)));
+}
+
+/* 添加一页,并在底部放一个分页指示点 */
+void SlidePage::addPage(QWidget *w)
+{
+    hBoxLayout->addWidget(w);
+    pageCount++;
+    QLabel *label = new QLabel();
+    label->setPixmap(QPixmap(":/icons/indicator1.png"));
+    pageIndicator.append(label);
+    bottomHBoxLayout->addWidget(label);
+}
+
+void SlidePage::resizeEvent(QResizeEvent *event)
+{
+    Q_UNUSED(event)
+    scrollArea->resize(this->size());
+    /* mainWidget 需要按页数横向撑开 */
+    mainWidget->resize(this->width() * pageCount, this->height() - 4);
+    if (pageCount == 0)
+        qDebug() << "当前页面总数为0,请使用addPage()方法添加页面再使用!" << endl;
+    else
+        onCurrentPageIndexChanged(0);
+    bottomWidget->setGeometry(0, this->height() - 20, this->width(), 20);
+}
+
+/* 松手:判断翻页方向并用属性动画平滑滚动 */
+void SlidePage::onStateChanged(QScroller::State state)
+{
+    static int pressedValue = 0, releasedValue = 0, currentPageIndex = 0;
+    if (pageCount == 0) return;
+
+    if (state == QScroller::Inactive) {          /* 松开 */
+        timer->stop();
+        releasedValue = QCursor::pos().x();
+        if (pressedValue == releasedValue) return;
+
+        if (!draggingFlag) {
+            if (pressedValue - releasedValue > 20 && currentPageIndex == pageIndex)
+                pageIndex++;
+            else
+                pageIndex--;
+        }
+        if (pageIndex == -1)          pageIndex = 0;
+        if (pageIndex >= pageCount)   pageIndex = pageCount - 1;
+
+        QPropertyAnimation *animation =
+                new QPropertyAnimation(scrollArea->horizontalScrollBar(), "value");
+        animation->setDuration(200);
+        animation->setStartValue(scrollArea->horizontalScrollBar()->value());
+        animation->setEasingCurve(QEasingCurve::OutCurve);
+        animation->setEndValue(pageIndex * this->width());
+        animation->start();
+
+        if (currentPageIndex != pageIndex)
+            emit currentPageIndexChanged(pageIndex);
+
+        pressedValue = releasedValue = 0;
+        draggingFlag = false;
+    }
+
+    if (state == QScroller::Pressed) {           /* 按下 */
+        pressedValue = QCursor::pos().x();
+        currentPageIndex = scrollArea->horizontalScrollBar()->value() / this->width();
+        timer->start(300);                       /* 超过 300ms 视为拖动 */
+    }
+}
+```
+
+### 16.2 主界面组装(`03_appmainview/widget.cpp`)
+
+```cpp
+AppMainView::AppMainView(QWidget *parent)
+{
+    this->setParent(parent);
+    this->setGeometry(0, 0, 800, 480);
+    this->setMinimumSize(800, 480);
+
+    bgWidget = new QWidget(this);
+    bgWidget->setStyleSheet("border-image: url(:/images/bg.png)");
+
+    mySlidePage = new SlidePage(this);          /* 使用滑动页面类 */
+    mySlidePage->resize(this->size());
+
+    for (int i = 0; i < 3; i++) {
+        appDemo[i] = new AppDemo();
+        mySlidePage->addPage(appDemo[i]);       /* 直接 addPage 添加页面 */
+    }
+}
+```
+
+`appdemo` 是一个车载音乐主界面的**纯界面**示例(顶部导航、中间专辑/分类/榜单、底部功能按钮),主要用嵌套布局与 `style.qss` 美化实现,不实现功能。底部三个分页逗点表示共有 3 页,左右滑动即可切换。
+
+## 17. 车牌识别项目(第二十七章)
+
+### 17.1 流程
+
+与语音识别类似,调用**百度 OCR 车牌识别**接口:把本地车牌图片(JPG)转成 base64 后 POST 到服务器,解析返回的 `words_result.number` 得到车牌号。使用前需在百度文字识别控制台创建应用,领取“车牌识别”免费资源,并填写自己的 API Key 与 Secret Key。
+
+```mermaid
+flowchart LR
+    A["image/carlpr.jpg"] --> B["QImage/QFile 读取<br/>toBase64().toPercentEncoding()"]
+    B --> C["image= + base64<br/>POST license_plate 接口"]
+    C --> D["解析 words_result"]
+    D --> E["取 number 字段"]
+    E --> F["界面显示车牌号"]
+
+    classDef s fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    class A,B,C,D,E,F s
+```
+
+### 17.2 识别端核心代码(`04/04_lpr_demo/ocr/ocr.cpp`)
+
+```cpp
+/* ocr.h 中的关键地址 */
+const QString token_org =
+    "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials"
+    "&client_id=%1&client_secret=%2&";
+const QString api_key    = "填写自己的APP KEY";
+const QString secret_key = "填写自己的APP SECRET";
+const QString server_api =
+    "https://aip.baidubce.com/rest/2.0/ocr/v1/license_plate?access_token=%1";
+
+/* 读取图片文件,转 base64 后作为表单体发送 */
+void Ocr::getTheResult(QString fileName)
+{
+    QFile file(fileName);
+    if (!file.exists()) { qDebug() << fileName << "不存在!" << endl; return; }
+
+    QByteArray requestData;
+    file.open(QIODevice::ReadOnly);
+    requestData = file.readAll();
+    file.close();
+
+    /* 转成 base64 再做 URL 百分号编码 */
+    QByteArray buf64 = requestData.toBase64().toPercentEncoding();
+    QByteArray body = "image=" + buf64;
+
+    serverApiUrl = QString(server_api).arg(accessToken);
+    requestNetwork(serverApiUrl, body);
+}
+
+/* 也支持直接传 QImage(可用摄像头采集的图片) */
+void Ocr::getTheResult(QImage image)
+{
+    QByteArray requestData;
+    QBuffer buffer(&requestData);
+    buffer.open(QIODevice::WriteOnly);
+    image.save(&buffer, "JPEG", -1);
+
+    QByteArray buf64 = requestData.toBase64().toPercentEncoding();
+    QByteArray body = "image=" + buf64;
+    serverApiUrl = QString(server_api).arg(accessToken);
+    requestNetwork(serverApiUrl, body);
+}
+
+/* 解析两级 JSON:words_result -> number */
+QString Ocr::getJsonValue(QByteArray ba, QString key1, QString key2)
+{
+    QJsonParseError parseError;
+    QJsonDocument jsonDocument = QJsonDocument::fromJson(ba, &parseError);
+    if (parseError.error == QJsonParseError::NoError
+            && jsonDocument.isObject()) {
+        QJsonObject jsonObj = jsonDocument.object();
+        if (jsonObj.contains(key1)) {
+            QJsonObject jsonArrObj = jsonObj.value(key1).toObject();
+            if (jsonArrObj.contains(key2))
+                return jsonArrObj.value(key2).toString();
+        }
+    }
+    return nullptr;
+}
+```
+
+### 17.3 界面与测试(`04_lpr_demo/mainwindow.cpp`)
+
+```cpp
+MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
+{
+    ocr = new Ocr(this);
+    this->setGeometry(0, 0, 800, 480);
+
+    QLabel *label = new QLabel(this);
+    resultLabel = new QLabel(this);
+    label->setAlignment(Qt::AlignCenter);
+
+    pushButton = new QPushButton(this);
+    pushButton->setText("开始识别");
+    resultLabel->move(pushButton->width(), 0);
+    resultLabel->resize(300, 30);
+
+    QPixmap pixmap;
+    if (pixmap.load("image/carlpr.jpg")) {
+        label->setPixmap(pixmap);
+    } else {
+        qDebug() << "未找要识别的车牌图片!请检查可执行程序下是否有image/carlpr.jpg" << endl;
+    }
+    this->setCentralWidget(label);
+
+    connect(pushButton, SIGNAL(clicked(bool)), this, SLOT(pushButtonClicked()));
+    connect(ocr, SIGNAL(ocrReadyData(QString)), this, SLOT(disPlaylprResult(QString)));
+}
+
+void MainWindow::pushButtonClicked()
+{
+    /* 识别可执行程序同级目录 image/carlpr.jpg */
+    ocr->readyToDetection("image/carlpr.jpg");
+}
+```
+
+运行前把 `image` 文件夹拷到可执行程序同级目录;需联网;适用于 Ubuntu18 与 I.MX6U 开发板(Ubuntu16 会报 OpenSSL 版本错误)。若识别不到则提示“未识别到车牌!”。也可改为用摄像头拍照后传 `QImage`。
+
+## 18. 视频监控项目(第二十八章)
+
+### 18.1 技术选型与流程
+
+流媒体领域常用 **RTSP**(基于文本、实时性好、适合视频监控/聊天,但实现复杂)与 **RTMP**(Adobe 提出、浏览器加载 flash 即可播放、低延迟)。I.MX6U 出厂系统支持 `RTMP + FFmpeg + Nginx` 推流。但本章用**最简单的方法**:服务端采集摄像头帧,经 UDP 广播发给客户端显示,不做视频编码,适合学习与毕设,缺点是数据量大。
+
+使用 **OV5640 摄像头**(支持 RGB 格式采集,与 Qt 显示格式一致,CPU 开销小);USB 摄像头采集 YUYV,转 RGB 会大量消耗 CPU,故不适配。使用 `QUdpSocket` 广播,可有多个客户端同时接收。
+
+```mermaid
+flowchart LR
+    CAM["OV5640 摄像头<br/>/dev/video1"] -->|V4L2 采集 RGB565| TH["CaptureThread<br/>(QThread 子线程)"]
+    TH -->|imageReady| LOCAL["服务端本地显示"]
+    TH -->|JPEG→base64→UDP 广播:8888| NET(("局域网"))
+    NET --> C1["客户端1<br/>QUdpSocket bind 8888"]
+    NET --> C2["客户端2"]
+    NET --> C3["客户端3"]
+
+    classDef s fill:#dbeafe,stroke:#2563eb,color:#1e3a5f
+    class CAM,TH,LOCAL,C1,C2,C3 s
+```
+
+### 18.2 服务端:采集线程(`video_server/capture_thread.h/.cpp`)
+
+```cpp
+/* capture_thread.h 关键定义 */
+#define VIDEO_DEV           "/dev/video1"
+#define FB_DEV              "/dev/fb0"
+#define VIDEO_BUFFER_COUNT  3
+
+struct buffer_info { void *start; unsigned int length; };
+
+class CaptureThread : public QThread
+{
+    Q_OBJECT
+signals:
+    void imageReady(QImage);    /* 准备本地显示 */
+    void sendImage(QImage);     /* 准备广播 */
+private:
+    bool startFlag = false, startBroadcast = false, startLocalDisplay = false;
+    void run() override;
+public slots:
+    void setThreadStart(bool start) {
+        startFlag = start;
+        if (start) { if (!this->isRunning()) this->start(); }
+        else       { this->quit(); }
+    }
+    void setBroadcast(bool start)    { startBroadcast = start; }
+    void setLocalDisplay(bool start) { startLocalDisplay = start; }
+};
+```
+
+```cpp
+/* capture_thread.cpp:V4L2 采集主循环(简化自源码,原理同 C 应用编程 V4L2 章节) */
+void CaptureThread::run()
+{
+#ifdef linux
+#ifndef __arm__
+    return;
+#endif
+    int video_fd = -1;
+    struct v4l2_format fmt;
+    struct v4l2_requestbuffers req_bufs;
+    static struct v4l2_buffer buf;
+    int n_buf;
+    struct buffer_info bufs_info[VIDEO_BUFFER_COUNT];
+    enum v4l2_buf_type type;
+
+    video_fd = open(VIDEO_DEV, O_RDWR);
+    if (0 > video_fd) return;
+
+    /* 设置格式:640x480,RGB565 */
+    fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    fmt.fmt.pix.width = 640;
+    fmt.fmt.pix.height = 480;
+    fmt.fmt.pix.colorspace = V4L2_COLORSPACE_SRGB;
+    fmt.fmt.pix.pixelformat = V4L2_PIX_FMT_RGB565;
+    if (0 > ioctl(video_fd, VIDIOC_S_FMT, &fmt)) { close(video_fd); return; }
+
+    /* 申请 3 个 mmap 缓冲区 */
+    req_bufs.count = VIDEO_BUFFER_COUNT;
+    req_bufs.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    req_bufs.memory = V4L2_MEMORY_MMAP;
+    if (0 > ioctl(video_fd, VIDIOC_REQBUFS, &req_bufs)) return;
+
+    buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    buf.memory = V4L2_MEMORY_MMAP;
+    for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
+        buf.index = n_buf;
+        ioctl(video_fd, VIDIOC_QUERYBUF, &buf);
+        bufs_info[n_buf].length = buf.length;
+        bufs_info[n_buf].start = mmap(NULL, buf.length, PROT_READ | PROT_WRITE,
+                                      MAP_SHARED, video_fd, buf.m.offset);
+        if (MAP_FAILED == bufs_info[n_buf].start) return;
+    }
+    for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
+        buf.index = n_buf;
+        ioctl(video_fd, VIDIOC_QBUF, &buf);
+    }
+    type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
+    ioctl(video_fd, VIDIOC_STREAMON, &type);
+
+    while (startFlag) {
+        for (n_buf = 0; n_buf < VIDEO_BUFFER_COUNT; n_buf++) {
+            buf.index = n_buf;
+            if (0 > ioctl(video_fd, VIDIOC_DQBUF, &buf)) return;
+
+            QImage qImage((unsigned char*)bufs_info[n_buf].start,
+                          fmt.fmt.pix.width, fmt.fmt.pix.height,
+                          QImage::Format_RGB16);
+
+            if (startLocalDisplay) emit imageReady(qImage);
+
+            if (startBroadcast) {
+                QUdpSocket udpSocket;
+                QByteArray byte;
+                QBuffer buff(&byte);
+                qImage.save(&buff, "JPEG", -1);           /* 压缩为 JPEG */
+                QByteArray base64Byte = byte.toBase64();  /* 转 base64 */
+                /* 以广播形式发送到 8888 端口 */
+                udpSocket.writeDatagram(base64Byte.data(), base64Byte.size(),
+                                        QHostAddress::Broadcast, 8888);
+            }
+
+            ioctl(video_fd, VIDIOC_QBUF, &buf);
+        }
+    }
+
+    msleep(800);
+    for (int i = 0; i < VIDEO_BUFFER_COUNT; i++)
+        munmap(bufs_info[i].start, buf.length);
+    close(video_fd);
+#endif
+}
+```
+
+服务端界面用两个 `QCheckBox` 分别控制“本地图像显示”和“UDP 广播”,一个按钮“开启采集摄像头数据”。注意:同时开启本地显示与广播会因 I.MX6U 性能有限而略卡,单独开启任一功能则很流畅。
+
+### 18.3 客户端:接收并显示(`video_client/mainwindow.cpp`)
+
+```cpp
+MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
+{
+    QColor color = QColor(Qt::black);
+    QPalette p;
+    p.setColor(QPalette::Window, color);
+    this->setPalette(p);
+
+    udpSocket = new QUdpSocket(this);
+    udpSocket->bind(QHostAddress::Any, 8888);      /* 绑定端口 */
+
+    videoLabel = new QLabel(this);
+    videoLabel->resize(640, 480);
+    videoLabel->setText("未获取到图像数据");
+    videoLabel->setStyleSheet("QWidget {color: white;}");
+    videoLabel->setAlignment(Qt::AlignCenter);
+
+    connect(udpSocket, SIGNAL(readyRead()), this, SLOT(videoUpdate()));
+    this->setGeometry(0, 0, 800, 480);
+}
+
+void MainWindow::videoUpdate()
+{
+    QByteArray datagram;
+    datagram.resize(udpSocket->pendingDatagramSize());
+    udpSocket->readDatagram(datagram.data(), datagram.size());
+
+    /* base64 解码后再还原为图像 */
+    QByteArray decryptedByte = QByteArray::fromBase64(datagram.data());
+    QImage image;
+    image.loadFromData(decryptedByte);
+    videoLabel->setPixmap(QPixmap::fromImage(image));
+}
+```
+
+客户端可在另一块开发板、Ubuntu 或 Windows 上运行,且可多个客户端同时运行;需与服务端在同一局域网。客户端显示的图像与服务端同步,效果良好。
+
+---
+
+# 面试题(5 道)
+
+### Q1. 嵌入式 Qt 交叉编译有哪两种方式?qmake 与 make 各自做什么?
+
+**答案要点**:命令行编译和 Qt Creator 交叉套件;qmake 读 `.pro` 生成 Makefile,make 调用交叉编译器完成编译链接。
+
+**详细解答**:命令行方式用交叉工具链的 `qmake` 生成 Makefile 后 `make`;Qt Creator 方式配置 ARM Kit(Qt Versions + 编译器)后可远程运行调试。跨平台的关键正是“换一个 qmake 就生成另一平台的 Makefile”。运行时用 `-platform linuxfb`(帧缓冲)或 `-platform eglfs`(EGL/OpenGL ES 全屏)选择显示插件。若远程运行结果异常(环境变量/路径问题),直接拷贝可执行文件到板上运行更稳妥。
+
+> ⚠️ **来源说明**:平台插件部分不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+### Q2. 为什么 Qt 控制 LED/BEEP 只要读写一个文件?写出关键步骤。
+
+**答案要点**:Linux 一切皆文件,出厂内核把外设注册为 gpio-leds;Qt 用 QFile 读写 `brightness` 节点,写 "1"/"0" 控制。
+
+**详细解答**:LED 节点 `/sys/devices/platform/leds/leds/sys-led/brightness`,BEEP 节点 `.../beep/brightness`。出厂系统 LED 默认是心跳触发,需先 `echo none > /sys/class/leds/sys-led/trigger`。`setLedState()` 先 `getLedState()` 读取当前值再取反写入,避免外部改动导致反转失效。AP3216C 等传感器也可用 C 的 `open()/read()` 读取 `/sys/class/misc/ap3216c/*`。
+
+### Q3. Qt 串口与 CAN 编程的流程分别是什么?各需要哪些 .pro 模块?
+
+**答案要点**:串口用 `QSerialPortInfo` 扫描 + `QSerialPort` 配置打开,模块 `serialport`;CAN 用 `QCanBus`/`QCanBusDevice`,模块 `serialbus`,Linux 下插件必须是 socketcan。
+
+**详细解答**:串口流程:`availablePorts()` 列串口 → `setPortName/setBaudRate/setDataBits/setParity/setStopBits/setFlowControl` → `open(QIODevice::ReadWrite)` → `readyRead()` 收、`write()` 发。I.MX6U 上只能用 `ttymxc2`。CAN 流程:先 `ip link set up can0 type can bitrate 1000000 restart-ms 100` 初始化系统 CAN(`QCanBusDevice::BitRateKey` 不能设比特率),再用 `QCanBus::instance()->createDevice("socketcan","can0")` 并 `connectDevice()`,`framesReceived()` 收、`writeFrame()` 发,帧 ID 用 16 进制、数据每字节空格分隔。
+
+### Q4. 视频监控/摄像头为什么必须开子线程?Qt 两种线程写法有何区别?
+
+**答案要点**:采集与编码耗时会阻塞主线程导致界面卡顿;继承 QThread 只有 run() 在新线程,moveToThread 可让整个 QObject 的槽函数都在新线程执行。
+
+**详细解答**:I.MX6U 是单核 A7,处理摄像头大图像吃力,必须把 V4L2 采集放在 `CaptureThread::run()` 中,界面通过信号接收 `QImage`。第二种写法是写一个继承 QObject 的 Worker,用 `moveToThread()` 把它移到 QThread,所有耗时操作写成槽函数供主线程调用,更灵活。退出线程用 `quit()` + `wait()`,不要用不安全的 `terminate()`。
+
+### Q5. 嵌入式 Qt 如何调用云端 API(智能家居/语音识别/车牌识别)?
+
+**答案要点**:用 Qt Network 走 HTTPS 获取 token 与数据,实时通信用 QWebSocket;音频/图片需符合接口的格式约束。
+
+**详细解答**:`QNetworkAccessManager` + `QNetworkRequest`/`QNetworkReply` 发 HTTPS 请求,访问 https 时需设置 `QSslConfiguration`(`VerifyNone`、`TlsV1SslV3`);原子云的实时消息用 `QWebSocket`(`wss://cloud.alientek.com/connection/...`)并鉴权后订阅(命令字 0x01)、下发(0x03)。百度语音要求 16000/8000 采样率、16bit 单声道、≤60s 的 wav,用 `audio/pcm;rate=16000`;车牌识别把图片 `toBase64().toPercentEncoding()` 后 POST。`.pro` 需加 `network` 模块。
+
+---
+
+**内容来源**:正点原子《I.MX6U嵌入式Qt开发指南V1.2》第十四章 I.MX6U Qt 开发、第十五章 Qt 控制 LED、第十六章 Qt 控制 BEEP、第十七章 Serial Port、第十八章 CAN Bus、第十九章 Camera、第二十章 USB Bluetooth、第二十一章 USER-KEY、第二十二章 AP3216C、第二十三章 ICM20608、第二十四章 智能家居物联网项目、第二十五章 语音识别项目、第二十六章 APP主界面开发项目、第二十七章 车牌识别项目、第二十八章 视频监控项目;配套例程 `Embedded-Qt-Tutorial/Qt/03`、`Qt/04`;平台插件 `-platform linuxfb/-platform eglfs` 部分为扩展知识。

+ 1120 - 0
X-Knowledge-Base/raw/Joplin/嵌入式+Linux/嵌入式Linux应用与Qt开发实战/05-Qt应用开发/面试-Qt应用开发.md

@@ -0,0 +1,1120 @@
+---
+title: 面试-Qt应用开发
+tags: [嵌入式Linux, 面试, Qt, C++, 面向对象, 类与对象, 继承, 重载, 多态, 虚函数, 命名空间, Qt Creator, qmake, ui文件, 信号与槽, connect, Q_OBJECT, 控件, QPushButton, QComboBox, QLineEdit, 布局, QBoxLayout, QGridLayout, 模型视图, QStandardItemModel, QListView, QTableView, QFile, QTextStream, QPainter, QChart, QThread, QMutexLocker, QTcpSocket, QTcpServer, QUdpSocket, QMediaPlayer, QSqlDatabase, QSqlTableModel, SQLite, 交叉编译, linuxfb, eglfs, I.MX6U]
+created: 2026-09-18
+updated: 2026-09-18
+pdf_ref: "《I.MX6U嵌入式Qt开发指南V1.2》第一章~第二十八章:在Ubuntu下编写C++、C++基础、初识Qt、使用Qt Designer开发、Qt信号与槽、Qt Creator使用技巧、Qt控件、文本读写、绘图与图表、多线程、网络编程、多媒体、数据库、I.MX6U Qt开发 ~ 视频监控项目"
+---
+
+# 面试-Qt应用开发
+
+> 💡 **关联知识**:[[05-Qt应用开发/01-C++基础与面向对象]]、[[05-Qt应用开发/02-Qt入门与项目结构]]、[[05-Qt应用开发/03-信号与槽]]、[[05-Qt应用开发/04-Qt控件详解]]、[[05-Qt应用开发/05-布局容器与模型视图]]、[[05-Qt应用开发/06-文本绘图与图表]]、[[05-Qt应用开发/07-多线程网络多媒体与数据库]]、[[05-Qt应用开发/08-嵌入式Qt交叉编译与项目实战]];延伸阅读:[[编程学习项目/使用Qt和C++构造UI界面/1. Qt概述]]、[[实际项目开发文档/加解密客户端(Qt+LibreSSL)/1.项目源代码]]
+
+覆盖《I.MX6U嵌入式Qt开发指南》全书 8 大主题、40 道题。每题含「答案要点 / 详细解答(代码、对比表)/ 2 条追问」,可直接用于复习、查询与面试自测。
+
+---
+
+## 一、C++基础(5 题)
+
+### Q1.1 C++ 相比 C 有哪些新特性?`cin/cout` 与 `scanf/printf` 有何区别?命名空间有什么用?
+
+**答案要点**:新增 bool 类型、变量可随用随定义并支持直接初始化;`cin/cout` 替代 `scanf/printf`,无需指定格式符;命名空间用于解决名字冲突。
+
+**详细解答**:
+
+- C++ 可直接 `int x(100);` 初始化,变量不必集中在函数开头定义。
+- 输入输出用 `cin >> x >> y;`、`cout << x << y << endl;`;`endl` 等价于换行 `\n`,但 `endl` 会刷新缓冲。效率上 `scanf/printf` 更高,便利性上 `cin/cout` 更好。注意变量间要用 `<<` 而非逗号。
+- 命名空间:标准库名字都加 `std::` 前缀,`using namespace std;` 后可直接写 `vector`、`cout`。也能自定义:
+
+```cpp
+namespace A {
+    int x = 1;
+    void fun() { cout << "A namespace" << endl; }
+}
+using namespace A;
+int main() {
+    fun();          /* 已声明 using,可直接调用 */
+    A::x = 3;       /* :: 表示作用域 */
+    A::fun();
+    return 0;
+}
+```
+
+- 注意 `#include <iostream>` 是标准流(无 `.h`,需命名空间);`#include <iostream.h>` 是非标准 C 库。
+
+**追问**
+1. `std::cin` 中的 `::` 是什么运算符?(作用域运算符,表示“std 命名空间下的”)
+2. 什么时候必须写全名而不用 `using namespace`?(库或公司自定义命名空间存在同名冲突时)
+
+### Q1.2 什么是类和对象?栈中与堆中实例化对象有何区别?构造函数和析构函数有什么特点?
+
+**答案要点**:类是对事物的抽象,对象是类的实例;栈上对象自动释放,堆上对象用 `new/delete` 手动管理;构造在实例化时自动调用一次,析构在生命周期结束时自动调用。
+
+**详细解答**:
+
+```cpp
+class Dog {
+public:
+    string name;
+    int age;
+    void run() { cout << name << " " << age << endl; }
+};
+
+Dog dog1;              /* 栈中实例化,函数结束自动析构 */
+dog1.name = "旺财"; dog1.age = 2;  dog1.run();
+
+Dog *dog2 = new Dog(); /* 堆中实例化,需手动 delete */
+dog2->name = "富贵";    dog2->age = 1; dog2->run();
+delete dog2; dog2 = NULL;
+```
+
+访问限定符:`public`(公有)、`private`(私有)、`protected`(保护),默认 `private`。
+
+构造函数特点:与类名同名、可重载、**没有返回类型(void 也不行)**、实例化时自动调用且仅一次。析构函数特点:`~类名()`、无参数、无返回值、生命周期结束时释放资源。类内实现或类外 `::` 实现均可。
+
+**追问**
+1. 不写构造函数和析构函数会怎样?(编译器会生成空的默认版本)
+2. `this` 指针是什么?(指向当前实例对象的指针,只能在成员函数使用,成员函数默认首参为 `T * const this`)
+
+### Q1.3 C++ 有哪几种继承方式?`protected` 继承后基类成员在派生类中的权限如何变化?
+
+**答案要点**:public / protected / private 三种;权限按“取更严格者”规则变化;默认为 private。
+
+**详细解答**:
+
+| 继承方式 | 基类 public | 基类 protected | 基类 private |
+| -------- | ----------- | -------------- | ------------ |
+| public | public | protected | 不可直接访问 |
+| protected | protected | protected | 不可直接访问 |
+| private | private | private | 不可直接访问 |
+
+```cpp
+class Animal { public: string color; int weight; };
+class Dog : public Animal { public: string name; int age; void run(); };
+/* Dog 的实例可直接使用继承来的 color / weight */
+```
+
+继承达到代码重用、提高效率的目的。Qt 中大量通过继承来重写/扩展已有类。
+
+**追问**
+1. 基类私有成员在派生类中能直接访问吗?(不能,但可通过基类公有/保护成员间接访问)
+2. 一个类能继承多个基类吗?(可以,多继承)
+
+### Q1.4 什么是函数重载和运算符重载?哪些运算符不能重载?
+
+**答案要点**:同一作用域内同名但参数列表不同的函数;运算符重载本质是函数重载,形式为 `operator 运算符(参数表)`;`.`、`::`、`sizeof`、`?:` 等不能重载。
+
+**详细解答**:
+
+函数重载:参数个数、类型或顺序必须不同,**仅返回类型不同不能构成重载**。
+
+```cpp
+class Dog {
+public:
+    void getWeight(int w)    { cout << w << "kG" << endl; }
+    void getWeight(double w) { cout << w << "kG" << endl; }
+};
+```
+
+运算符重载:
+
+```cpp
+class Dog {
+public:
+    int weight;
+    Dog operator+(const Dog &d) {      /* 重载 + */
+        Dog dog;
+        dog.weight = this->weight + d.weight;
+        return dog;
+    }
+};
+```
+
+不可重载:成员访问 `.`、成员指针访问 `.*` 与 `->*`、域运算符 `::`、`sizeof`、条件运算符 `?:`、预处理符号 `#`。
+
+**追问**
+1. 为什么要重载运算符?(让自定义类型也能像基本类型一样使用 `+`、`==` 等,形式统一)
+2. 重载时对象属性如何访问?(通过 `this`,因为对象是作为参数传入的)
+
+### Q1.5 什么是多态?形成多态的三个条件是什么?虚函数与纯虚函数有何区别?
+
+**答案要点**:同一调用根据对象实际类型执行不同函数;条件为继承、同名虚函数、基类指针/引用调用;虚函数必须有实现,纯虚函数 `=0` 无实现、使类成为抽象类。
+
+**详细解答**:
+
+形成多态必须具备三个条件:① 存在继承关系;② 继承关系中有同名虚函数(基类用 `virtual` 声明);③ 存在基类类型的指针或引用,通过它调用虚函数。
+
+```cpp
+class Animal { public: virtual void run() { cout << "Animal" << endl; } };
+class Dog : public Animal { public: void run() { cout << "Dog" << endl; } };
+class Cat : public Animal { public: void run() { cout << "Cat" << endl; } };
+
+Animal *animal;          /* 基类指针,未实例化 */
+Dog dog; Cat cat;
+animal = &dog; animal->run();   /* 输出 Dog */
+animal = &cat; animal->run();   /* 输出 Cat */
+```
+
+- **虚函数**:`virtual ReturnType Fun(Param)`,必须实现;使编译器动态绑定(后期绑定)。
+- **纯虚函数**:`virtual void funtion1() = 0;`,没有实现,用来规范派生类接口;含纯虚函数的类是**抽象类**,不能实例化,只能作接口。
+
+**追问**
+1. 去掉 `virtual` 会怎样?(静态链接到基类函数,调用基类自己的 `run()`,多态失效)
+2. `QWidget` 的 `paintEvent()` 为什么能被子类重写?(它在基类中定义为虚函数,这正是多态的典型应用)
+
+---
+
+## 二、Qt入门与项目结构(5 题)
+
+### Q2.1 什么是 Qt?Qt 与 Qt Creator 是什么关系?Qt C++ 与 QML 如何选择?
+
+**答案要点**:Qt 是跨平台 C++ 开发库(不止 GUI);Qt Creator 是 IDE;Qt C++ 功能强、成熟,QML 适合做炫酷界面,二者常结合使用。
+
+**详细解答**:Qt 除了控件、布局、交互,还内置多线程、数据库、图像/音视频、网络、文件等模块;支持 Windows/Linux/Unix/Android/iOS/QNX/VxWorks 等平台,一份代码稍作修改即可跨平台运行。Qt 还有 Python、Ruby 等绑定。Qt Creator 是官方 IDE,负责编辑、编译、调试、UI 设计。
+
+Qt C++ 效率更高、底层功能突出;QML 是描述性语言,配合 Qt Quick 做移动端风格的界面,但底层(文件读写、图像处理、多线程通信)仍靠 C++。低版本(Qt5.6 以下)QML 不够成熟,Qt5.7 以上更完善。本教程主打 **Qt C++**。
+
+**追问**
+1. “跨平台”对 Qt 意味着什么?(同一份源码在不同平台用不同套件编译即可运行)
+2. 嵌入式里为什么常用 QWidget 作为基类?(它不带标题栏/状态栏,适合全屏界面)
+
+### Q2.2 一个 Qt Widgets 工程由哪些文件组成?三种基类 `QMainWindow/QWidget/QDialog` 如何选?
+
+**答案要点**:`.pro`(工程)、`.h`(声明)、`.cpp`(实现)、`.ui`(界面);`QMainWindow` 带菜单/工具栏/状态栏,`QWidget` 是界面基类且最“轻”,`QDialog` 是对话框。
+
+**详细解答**:
+
+| 文件 | 作用 |
+| ---- | ---- |
+| `*.pro` | 项目管理文件,增删文件时 Qt Creator 自动维护,也可手动加配置 |
+| `*.h` | 类的声明;用到信号的类必须有 `Q_OBJECT` |
+| `*.cpp` | 类的实现 |
+| `*.ui` | XML 描述的界面文件,编译时生成 `ui_*.h` |
+
+基类选择:
+
+- `QMainWindow`:主窗口,自带菜单栏、工具栏、状态栏,适合嵌套窗口程序;
+- `QWidget`:可视界面类的基类,`QMainWindow` 由它继承而来,功能更少、更轻,**嵌入式常选它**(不要标题栏/状态栏);
+- `QDialog`:对话框,较少作为主基类。
+
+```cpp
+#include "mainwindow.h"
+#include <QApplication>
+int main(int argc, char *argv[]) {
+    QApplication a(argc, argv);   /* 每个 Qt 程序必须有一个 */
+    MainWindow w;
+    w.show();
+    return a.exec();              /* 进入事件循环 */
+}
+```
+
+**追问**
+1. 为什么 Qt 程序的 `main()` 最后是 `a.exec()`?(把控制权交给 Qt,进入事件循环处理用户/系统事件)
+2. `QMainWindow` 与 `QWidget` 的继承关系?(`QMainWindow` 继承自 `QWidget`)
+
+### Q2.3 解释 `.pro` 文件中常见条目的含义。
+
+**答案要点**:`QT +=` 引入模块,`greaterThan(...)` 按 Qt 大版本追加模块,`CONFIG += c++11` 用 C++11,`TARGET` 指定可执行文件名,`SOURCES/HEADERS/FORMS` 管理文件。
+
+**详细解答**:
+
+```qmake
+QT       += core gui                              # 默认模块
+greaterThan(QT_MAJOR_VERSION, 4): QT += widgets   # Qt5 需加 widgets
+CONFIG   += c++11
+DEFINES  += QT_DEPRECATED_WARNINGS
+TARGET   = 01_led                                 # 不写则默认工程名
+SOURCES += main.cpp mainwindow.cpp
+HEADERS += mainwindow.h
+FORMS   += mainwindow.ui
+QT       += serialport                            # 串口模块
+QT       += serialbus                             # CAN 模块
+QT       += charts                                # 图表模块
+QT       += multimedia                            # 多媒体模块
+QT       += sql                                   # 数据库模块
+QT       += network                               # 网络模块
+```
+
+第 1 行 core 与 gui 是默认设置;部署规则 `qnx: ... / else: unix:!android: ...` 用于指定安装路径。
+
+**追问**
+1. 要用 Qt 串口需要在 `.pro` 加什么?(`QT += core gui serialport`)
+2. `.pri` 文件是什么?(语法同 `.pro`,可被 `include()` 引入,便于复用与拆分大型工程)
+
+### Q2.4 `.ui` 文件是如何被程序使用的?`ui_mainwindow.h`、`setupUi()`、`Ui::MainWindow` 各是什么?
+
+**答案要点**:`.ui` 由设计器生成 XML,编译时生成 `ui_mainwindow.h`;`Ui::MainWindow` 是界面对象类,`setupUi(this)` 负责把界面控件实例化并挂到主窗口。
+
+**详细解答**:
+
+```cpp
+/* mainwindow.h */
+QT_BEGIN_NAMESPACE
+namespace Ui { class MainWindow; }     /* 前置声明,注意与下面的 MainWindow 不同 */
+QT_END_NAMESPACE
+
+class MainWindow : public QMainWindow {
+    Q_OBJECT
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+private:
+    Ui::MainWindow *ui;                /* 指向界面对象的指针 */
+};
+
+/* mainwindow.cpp */
+MainWindow::MainWindow(QWidget *parent)
+    : QMainWindow(parent), ui(new Ui::MainWindow) {
+    ui->setupUi(this);                 /* 界面初始化,控件以 this 为父对象 */
+}
+MainWindow::~MainWindow() {
+    delete ui;                         /* new 且无父对象的对象需手动 delete */
+}
+```
+
+`ui_mainwindow.h` 是**构建后才生成**的(位于 build 目录),所以使用 UI 设计的信号槽连接,需要编译后才能在 `setupUi()` 里看到。
+
+**追问**
+1. 为什么使用 `ui->` 里控件必须写在 `setupUi(this)` 之后?(该调用才完成控件初始化)
+2. `Q_OBJECT` 宏的作用?(Qt 元对象系统的基础,凡用到信号槽的类都要加)
+
+### Q2.5 用 Qt Designer(UI 文件)开发和纯代码开发各有什么优缺点?Qt Creator 有哪些实用技巧?
+
+**答案要点**:Designer 直观快捷但控件多时难管理、信号槽连接不直观;纯代码利于管理与审查、能锻炼布局能力;Qt Creator 帮助文档和快捷键很实用。
+
+**详细解答**:
+
+- Designer 优点:拖拽布局、直观快捷;缺点:控件多时不易管理,必须打开 ui 文件加控件,信号槽连接要么编译后在 `setupUi()` 看、要么进设计器看,不直观。
+- 纯代码优点:便于代码管理与版本控制,布局与逻辑一目了然;缺点:需要熟悉各控件 API。
+- 本教程从第五章起统一用代码绘制界面。
+
+Qt Creator 技巧:左侧“帮助”可查类/方法/信号槽用法;`Ctrl + N` 新建项目;`Ctrl + R` 编译并运行;`Ctrl + Shift + T` 查看类的继承关系;`F1` 查看光标处符号的帮助;按住 `Ctrl` 点击符号跳转到定义。
+
+**追问**
+1. 无 UI 文件时如何设置窗口大小?(`this->resize(800, 480);` 或 `setGeometry(0,0,800,480)`)
+2. 嵌入式常用的分辨率是多少?(本教程统一 800×480)
+
+---
+
+## 三、信号与槽(5 题)
+
+### Q3.1 什么是信号与槽?`connect()` 的基本语法是什么?`SIGNAL`/`SLOT` 宏的作用?
+
+**答案要点**:信号是特定情况下发射的事件,槽是响应函数;`QObject::connect(sender, SIGNAL(sig()), receiver, SLOT(slot()))`;两个宏把函数签名转成字符串。
+
+**详细解答**:信号(Signal)是特定情况下被发射的事件,如按钮的 `clicked()`、下拉框的 `currentIndexChanged()`。槽(Slot)是对信号响应的函数,本质是普通 C++ 函数,可定义在 `public/protected/private` 下,可带参数,也能被直接调用;特殊之处在于能与信号关联,信号发射时自动执行。
+
+```cpp
+connect(sender, SIGNAL(clicked()), receptor, SLOT(close()));
+connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
+```
+
+`connect()` 是 `QObject` 的静态函数,`QObject` 是所有 Qt 类的基类,故常省略限定符。`SIGNAL`/`SLOT` 把信号和槽的参数转换为相应字符串。带参时只需写参数类型,可省略参数名。
+
+**追问**
+1. `connect` 中发送者与接收者的顺序能颠倒吗?(不能,sender 与信号在一起,receiver 与槽在一起)
+2. 信号与槽的参数必须完全一致吗?(至少信号参数不能少于槽参数,否则编译或运行错误)
+
+### Q3.2 信号与槽有哪些连接规则?
+
+**答案要点**:一个信号可连多个槽、多个信号可连同一个槽、一个信号可连另一个信号;槽按连接顺序执行;使用的类必须加 `Q_OBJECT`;信号发射后槽立即执行,全部槽执行完才继续。
+
+**详细解答**:
+
+```cpp
+/* 一对多:按连接顺序依次执行 */
+connect(pushButton, SIGNAL(clicked()), this, SLOT(hide()));
+connect(pushButton, SIGNAL(clicked()), this, SLOT(close()));
+
+/* 多对一:任一按钮点击都关闭 */
+connect(pushButton_2, SIGNAL(clicked()), this, SLOT(close()));
+connect(pushButton_3, SIGNAL(clicked()), this, SLOT(close()));
+
+/* 信号连信号:一个信号发射会触发另一个信号 */
+connect(pushButton, SIGNAL(objectNameChanged(QString)),
+        this, SIGNAL(windowTitleChanged(QString)));
+```
+
+要点:严格情况下参数个数与类型需一致;类中必须加 `Q_OBJECT` 宏;信号发射时槽立即执行,像普通函数调用一样,全部槽完成才执行发射点之后的代码。
+
+**追问**
+1. 不写 `Q_OBJECT` 会怎样?(信号槽机制无法工作,编译或链接报错)
+2. 一个信号连多个槽时,执行顺序如何?(按建立连接的先后顺序)
+
+### Q3.3 如何在项目中自定义信号?为什么信号只需声明?
+
+**答案要点**:在类中 `signals:` 下声明,只需声明无需实现;使用时用 `emit` 发射;信号无返回类型。
+
+**详细解答**:
+
+```cpp
+class MainWindow : public QMainWindow {
+    Q_OBJECT
+public:
+    MainWindow(QWidget *parent = nullptr);
+    ~MainWindow();
+signals:
+    /* 声明一个信号,只需声明,无需定义 */
+    void pushButtonTextChanged();
+};
+```
+
+信号由 moc 自动生成实现,因此不能也不必自己写函数体;信号没有 `public/protected/private` 概念,名称建议贴合含义(如“按钮文本改变后”)。
+
+**追问**
+1. 发射信号用什么关键字?(`emit`,如 `emit pushButtonTextChanged();`)
+2. 信号能有返回值吗?(不能)
+
+### Q3.4 如何在项目中自定义槽?槽函数有什么特点?
+
+**答案要点**:在类中 `slots:` 下声明,并在 cpp 中实现(必须实现);槽可以是任意成员函数、普通全局函数或静态函数;参数和返回值要与信号匹配。
+
+**详细解答**:
+
+```cpp
+class MainWindow : public QMainWindow {
+    Q_OBJECT
+public:
+    MainWindow(QWidget *parent = nullptr);
+public slots:
+    void changeButtonText();     /* 声明槽 */
+    void pushButtonClicked();
+private:
+    QPushButton *pushButton;
+};
+
+/* 实现槽:槽里改变按钮文本 */
+void MainWindow::changeButtonText() {
+    pushButton->setText("被点击了!");
+}
+/* 实现按钮点击槽:用 emit 发射自定义信号 */
+void MainWindow::pushButtonClicked() {
+    emit pushButtonTextChanged();
+}
+
+MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) {
+    this->resize(800, 480);
+    pushButton = new QPushButton(this);
+    pushButton->setText("我是一个按钮");
+    connect(pushButton, SIGNAL(clicked()), this, SLOT(pushButtonClicked()));
+    connect(this, SIGNAL(pushButtonTextChanged()), this, SLOT(changeButtonText()));
+}
+```
+
+槽特点:① 可以是任何成员函数、普通全局函数、静态函数;② 与信号的参数和返回值要一致。本例流程:点击按钮 → `pushButtonClicked()` → 发射 `pushButtonTextChanged()` → `changeButtonText()` 响应,按钮文本变为“被点击了!”。
+
+**追问**
+1. 槽函数只声明不实现行吗?(不行,编译报错)
+2. `QPushButton` 还有哪些构造重载?(`QPushButton(const QString &text, QWidget *parent)`、`QPushButton(const QIcon &icon, const QString &text, QWidget *parent)`)
+
+### Q3.5 如何断开信号与槽?`disconnect()` 有哪几种常用形式?
+
+**答案要点**:用 `disconnect()`,可断开某对象的全部连接、某信号的全部连接、或与指定接收者的连接。
+
+**详细解答**:
+
+```cpp
+/* 断开 myObject 的一切连接 */
+disconnect(myObject, 0, 0, 0);        /* 等价 myObject->disconnect(); */
+
+/* 断开连接到 mySignal() 的全部连接 */
+disconnect(myObject, SIGNAL(mySignal()), 0, 0);  /* 等价 myObject->disconnect(SIGNAL(mySignal())); */
+
+/* 与指定接收者断开 */
+disconnect(myObject, 0, myReceiver, 0);          /* 等价 myObject->disconnect(myReceiver); */
+```
+
+初学者一般很少主动断开连接;但动态创建/销毁对象时要注意避免悬空连接。
+
+**追问**
+1. Qt4 与 Qt5 的 connect 写法有何区别?(Qt5 支持函数指针语法,编译期检查,更安全;Qt4 依赖 `SIGNAL/SLOT` 字符串,运行期解析)
+2. 对象被销毁后其连接会自动清理吗?(QObject 析构时会自动断开相关连接)
+
+---
+
+## 四、控件(5 题)
+
+### Q4.1 Qt 的按钮类控件有哪些?各自常用信号是什么?
+
+**答案要点**:`QPushButton`(clicked)、`QToolButton`(带图标/菜单)、`QRadioButton`(toggled,互斥)、`QCheckBox`(stateChanged,三态)、`QCommandLinkButton`、`QDialogButtonBox`。
+
+**详细解答**:
+
+| 控件 | 特点 | 常用信号 |
+| ---- | ---- | -------- |
+| QPushButton | 普通按钮,可显示文本/图标 | `clicked()` |
+| QToolButton | 工具按钮,常用于工具栏,可带下拉菜单 | `clicked()` |
+| QRadioButton | 单选按钮,同组互斥 | `toggled(bool)` |
+| QCheckBox | 复选按钮,支持三态 | `stateChanged(int)` |
+| QCommandLinkButton | 命令链接按钮,Vista 风格 | `clicked()` |
+| QDialogButtonBox | 对话框标准按钮集合 | `clicked(QAbstractButton*)` |
+
+常用方法:`setText()`、`setIcon()`、`setCheckable()`、`setChecked()`、`setEnabled()`、`setMinimumSize()`。
+
+**追问**
+1. `setCheckable(true)` 有什么效果?(按钮变为可选中/取消的切换状态,如“开始/关闭”)
+2. 单选按钮如何分组保证互斥?(放在同一父容器或用 `QButtonGroup`)
+
+### Q4.2 输入类控件有哪些?`QComboBox`、`QLineEdit`、`QSpinBox` 常用方法是什么?
+
+**答案要点**:`QComboBox`、`QFontComboBox`、`QLineEdit`、`QTextEdit`、`QPlainTextEdit`、`QSpinBox`、`QDoubleSpinBox`、`QDateTimeEdit`、`QDial`、`QScrollBar`、`QSlider`、`QKeySequenceEdit`。
+
+**详细解答**:
+
+| 控件 | 用途 | 常用方法 |
+| ---- | ---- | -------- |
+| QComboBox | 下拉选择 | `addItem()`、`addItems()`、`setCurrentIndex()`、`currentText()` |
+| QLineEdit | 单行输入 | `setText()`、`text()`、`setEchoMode()`、`setPlaceholderText()` |
+| QTextEdit | 多行富文本 | `setPlainText()`、`toPlainText()`、`insertPlainText()` |
+| QPlainTextEdit | 多行纯文本 | `setPlainText()`、`appendPlainText()` |
+| QSpinBox | 整数微调 | `setRange()`、`setValue()`、`value()`、`valueChanged(int)` |
+| QDoubleSpinBox | 浮点微调 | 同上 |
+| QDateTimeEdit | 日期时间 | `setDateTime()`、`dateTime()` |
+| QDial / QSlider | 旋钮 / 滑块 | `setValue()`、`valueChanged(int)` |
+
+**追问**
+1. `QLineEdit` 密码框怎么实现?(`setEchoMode(QLineEdit::Password)`)
+2. `QComboBox` 如何给选项附带数据?(`addItem(text, userData)` + `currentData()`)
+
+### Q4.3 显示类控件有哪些?`QLabel` 如何显示图片?`QProgressBar` 如何更新进度?
+
+**答案要点**:`QLabel`、`QCalendarWidget`、`QLCDNumber`、`QProgressBar`、`QFrame`、`QTextBrowser`、`QGraphicsView`;`QLabel::setPixmap()` 显示图片,`setScaledContents(true)` 自适应;`QProgressBar::setValue()` 更新进度。
+
+**详细解答**:
+
+```cpp
+QLabel *label = new QLabel(this);
+label->setPixmap(QPixmap(":/images/openedv.png"));
+label->setScaledContents(true);      /* 图片随控件缩放 */
+
+QProgressBar *bar = new QProgressBar(this);
+bar->setRange(0, 100);
+bar->setValue(50);                    /* 更新到 50% */
+```
+
+`QLCDNumber` 显示数字风格的数值,`QCalendarWidget` 提供日历,`QTextBrowser` 支持富文本浏览(带链接),`QGraphicsView` 用于图形场景显示。
+
+**追问**
+1. `QLabel` 的自动换行怎么设置?(`setWordWrap(true)`)
+2. 图像资源为什么要放进 `.qrc`?(编译进可执行文件,运行时用 `:/` 前缀访问,避免路径问题)
+
+### Q4.4 容器类控件有哪些?`QTabWidget` 与 `QStackedWidget`、`QListWidget` 如何配合?
+
+**答案要点**:`QGroupBox`、`QScrollArea`、`QToolBox`、`QTabWidget`、`QStackedWidget`、`QMdiArea`、`QDockWidget`;`QStackedWidget` 常与 `QListWidget` 通过 `setCurrentIndex(int)` 槽联动。
+
+**详细解答**:
+
+```cpp
+/* 左边列表,右边堆叠页面,点击列表切换页面 */
+stackedWidget = new QStackedWidget();
+listWidget = new QListWidget();
+connect(listWidget, SIGNAL(currentRowChanged(int)),
+        stackedWidget, SLOT(setCurrentIndex(int)));
+
+/* QTabWidget:直接 addTab 添加带标签页 */
+tabWidget->addTab(pageWidget, "蓝牙聊天");
+tabWidget->addTab(pageWidget2, "蓝牙列表");
+tabWidget->setCurrentIndex(0);
+```
+
+`QGroupBox` 用于给一组控件加标题边框;`QScrollArea` 提供滚动区域(内容大于可视区时使用);`QToolBox` 是抽屉式分组;`QTabWidget` 是标签页;`QStackedWidget` 是“同一时间只显示一页”的堆叠容器。
+
+**追问**
+1. `QStackedWidget` 与 `QTabWidget` 的本质关系?(`QTabWidget` 内部基于堆叠布局,多了标签栏)
+2. 内容超出窗口时用什么容器?(`QScrollArea`)
+
+### Q4.5 “基于项”的控件与“基于模型”的控件有什么区别?各举三例。
+
+**答案要点**:基于模型的控件(`QListView/QTreeView/QTableView/QColumnView`)由外部 Model 提供数据,适合大数据与多视图共享;基于项的控件(`QListWidget/QTreeWidget/QTableWidget`)自带内部模型,直接用 `addItem` 等操作,简单方便。
+
+**详细解答**:
+
+| 维度 | 基于模型(View) | 基于项(Widget) |
+| ---- | ---------------- | ---------------- |
+| 代表 | QListView、QTreeView、QTableView | QListWidget、QTreeWidget、QTableWidget |
+| 数据来源 | 外部 Model(如 QStandardItemModel) | 内部自带模型 |
+| 操作方式 | `setModel()` | `addItem()`、`setItem()` |
+| 适用 | 大数据、复杂结构、多视图共享 | 中小数据、快速开发 |
+
+`QListWidget` 继承 `QListView`,`QTreeWidget` 继承 `QTreeView`,`QTableWidget` 继承 `QTableView`——即“控件版”是“视图版”的便捷封装。
+
+**追问**
+1. 一般处理大数据用哪种?(基于模型的 View,避免为每项创建对象)
+2. 列表项如何放置自定义控件?(`QListWidget::setItemWidget(item, widget)`)
+
+## 五、布局与模型视图(5 题)
+
+### Q5.1 为什么推荐用布局管理器而不是绝对定位?Qt 有哪些布局类?
+
+**答案要点**:布局能随窗口大小自动调整、适配不同分辨率、便于维护;主要有 `QBoxLayout`(含 `QHBoxLayout/QVBoxLayout`)、`QGridLayout`、`QFormLayout`。
+
+**详细解答**:绝对定位(`setGeometry`/`move`)写死后,窗口缩放或换屏幕分辨率时界面就错乱;嵌入式设备分辨率各异,布局管理是必须。常见布局:
+
+| 布局 | 排列方式 | 典型场景 |
+| ---- | -------- | -------- |
+| QHBoxLayout | 水平 | 一排按钮、标签+输入框 |
+| QVBoxLayout | 垂直 | 上下分区 |
+| QGridLayout | 网格 | 串口/CAN 参数表 |
+| QFormLayout | 两列表单 | 登录、设置表单 |
+
+**追问**
+1. 嵌入式里界面的基准分辨率教材用多少?(800×480)
+2. 布局能嵌套吗?(能,布局内可再 `addLayout`,实现复杂界面)
+
+### Q5.2 写出布局的常用 API。`QSpacerItem` 有什么用?
+
+**答案要点**:`addWidget()`、`addLayout()`、`addStretch()`、`setContentsMargins()`、`setSpacing()`、`setAlignment()`;`QSpacerItem` 在布局中提供空白,用于撑开或对齐。
+
+**详细解答**:
+
+```cpp
+QVBoxLayout *vboxLayout = new QVBoxLayout();
+vboxLayout->addWidget(textBrowser);
+vboxLayout->addWidget(textEdit);
+vboxLayout->setContentsMargins(0, 0, 0, 0);   /* 左上右下外边距 */
+vboxLayout->setSpacing(10);                    /* 控件间距 */
+mainWidget->setLayout(vboxLayout);
+
+/* 网格布局:把标签放 (0,i),下拉框放 (1,i) */
+gridLayout->addWidget(label[i], 0, i);
+gridLayout->addWidget(comboBox[i], 1, i);
+
+/* 空间间隔 */
+QSpacerItem *hSpacer = new QSpacerItem(10, 10,
+        QSizePolicy::Expanding, QSizePolicy::Minimum);
+hBoxLayout->addSpacerItem(hSpacer);
+```
+
+`QSpacerItem` 继承 `QLayoutItem`,含水平/垂直间隔,用来把控件挤向一侧或在控件之间留白。
+
+**追问**
+1. `addStretch()` 与 `QSpacerItem` 的区别?(前者是快捷方式,本质也是插入可伸缩间隔)
+2. `setContentsMargins` 与 `setSpacing` 分别控制什么?(布局四周外边距 / 相邻控件间距)
+
+### Q5.3 Qt 模型/视图框架由哪几部分组成?`QStandardItemModel` 如何设置表头与数据?
+
+**答案要点**:Model(数据)、View(显示)、Delegate(渲染与编辑)三部分;`QStandardItemModel` 是通用模型,`setHorizontalHeaderLabels()` 设表头,`appendRow()` 逐行添加。
+
+**详细解答**:
+
+```cpp
+QStandardItemModel *model = new QStandardItemModel();
+QStringList labels;
+labels << "姓名" << "年龄" << "性别";
+model->setHorizontalHeaderLabels(labels);        /* 设置列数与表头文字 */
+
+/* 逐行添加数据 */
+QList<QStandardItem *> row;
+row << new QStandardItem("啊万") << new QStandardItem("27") << new QStandardItem("男");
+model->appendRow(row);
+
+tableView->setModel(model);                       /* 视图绑定模型 */
+```
+
+好处是数据与显示分离:同一份 Model 可被 List/Tree/Table 多种 View 共享,也可加 `QSortFilterProxyModel` 做排序过滤。
+
+**追问**
+1. Delegate 的作用?(决定每一项如何绘制、如何进入编辑状态,如自定义进度条单元格)
+2. 视图只读怎么设置?(`setEditTriggers(QAbstractItemView::NoEditTriggers)`)
+
+### Q5.4 `QListView`、`QTreeView`、`QTableView` 如何填充数据?
+
+**答案要点**:三者都继承 `QAbstractItemView`,通过 `setModel()` 绑定模型;列表用 `QStringListModel` 或 `QStandardItemModel`,树/表用 `QStandardItemModel` 的 `appendRow`/`setItem`。
+
+**详细解答**:
+
+```cpp
+/* 列表视图:用字符串列表模型 */
+QStringList list; list << "A" << "B" << "C";
+QStringListModel *stringListModel = new QStringListModel(list);
+QListView *listView = new QListView;
+listView->setModel(stringListModel);
+listView->setViewMode(QListView::IconMode);   /* 图标模式 */
+
+/* 表视图:先设表头,再用 setItem(row, col, item) */
+QStandardItemModel *tableModel = new QStandardItemModel();
+tableModel->setHorizontalHeaderLabels(QStringList() << "编号" << "姓名");
+for (int r = 0; r < 3; r++)
+    for (int c = 0; c < 2; c++)
+        tableModel->setItem(r, c, new QStandardItem(QString::number(r * 2 + c)));
+tableView->setModel(tableModel);
+
+/* 树视图:一级标题 appendRow,子项用父项 appendRow */
+QStandardItemModel *treeModel = new QStandardItemModel(treeView);
+treeModel->setHorizontalHeaderLabels(QStringList() << "分类" << "说明");
+QStandardItem *root = new QStandardItem("传感器");
+root->appendRow(new QStandardItem("AP3216C"));
+treeModel->appendRow(root);
+treeView->setModel(treeModel);
+```
+
+**追问**
+1. `QTreeView` 如何展开全部节点?(`expandAll()`)
+2. `QTableView` 如何隐藏网格线?(`setShowGrid(false)`)
+
+### Q5.5 “基于项”的 `QListWidget/QTreeWidget/QTableWidget` 如何用?怎样在列表项里放自定义控件?
+
+**答案要点**:直接 `addItem()`/`setItem()`;`QTableWidget` 先 `setRowCount/setColumnCount` 再 `setItem(row,col,new QTableWidgetItem(...))`;列表项内嵌控件用 `setItemWidget(item, widget)`。
+
+**详细解答**:
+
+```cpp
+/* QListWidget:添加文本项 */
+QListWidget *listWidget = new QListWidget(this);
+listWidget->addItem("0.mp3");
+listWidget->addItem("1.mp3");
+
+/* QTableWidget:设置行列与表头,再填单元格 */
+QTableWidget *tableWidget = new QTableWidget(3, 2, this);
+tableWidget->setHorizontalHeaderLabels(QStringList() << "姓名" << "年龄");
+tableWidget->setItem(0, 0, new QTableWidgetItem("啊万"));
+tableWidget->setItem(0, 1, new QTableWidgetItem("27"));
+
+/* 在列表项中嵌入自定义 Widget(智能家居的控制卡片就是这么做的) */
+QListWidgetItem *item = new QListWidgetItem();
+listWidget->addItem(item);
+listWidget->setItemWidget(item, customWidget);
+```
+
+基于项控件本质是“封装好模型的视图”,操作直观;数据量大时应改用 View + Model。
+
+**追问**
+1. `QTableWidget` 如何让表格自动适应内容?(`resizeColumnsToContents()`)
+2. `QListWidget` 单击切换页面用什么信号?(`currentRowChanged(int)` 连 `QStackedWidget::setCurrentIndex`)
+
+---
+
+## 六、绘图、文件与图表(5 题)
+
+### Q6.1 `QFile` 打开文件有哪些模式?常用方法有哪些?路径分隔符要注意什么?
+
+**答案要点**:`ReadOnly/WriteOnly/ReadWrite/Append/Truncate/Text` 可组合;`open/close/flush/exists/remove/size/pos/seek/atEnd`;文件路径统一用 `/`。
+
+**详细解答**:`QFile` 是读写文本、二进制和资源的 I/O 设备,可单独用,也可配合 `QTextStream/QDataStream`。
+
+```cpp
+QFile file("/sys/devices/platform/leds/leds/sys-led/brightness");
+if (!file.exists()) return;
+if (!file.open(QIODevice::ReadWrite))
+    qDebug() << file.errorString();
+file.write("1");
+file.close();
+```
+
+| 模式 | 含义 |
+| ---- | ---- |
+| ReadOnly | 只读 |
+| WriteOnly | 只写 |
+| ReadWrite | 读写 |
+| Append | 追加到尾部 |
+| Truncate | 清空原内容 |
+| Text | 文本模式,`\n` 自动翻译 |
+
+**关键点**:无论 Windows、Linux 还是 Mac,`QFile` 的分隔符都是 `/`,不要用 `\`。
+
+**追问**
+1. 嵌入式里为什么常用 QFile 直接操作 `/sys` 节点?(Linux 一切皆文件,无需驱动即可控制 GPIO/LED)
+2. 如何读取全部内容?(`file.readAll()`,文本模式配合 `QTextStream::readAll()`)
+
+### Q6.2 `QTextStream` 与 `QFile` 有何区别?什么时候用 `QTextStream`?
+
+**答案要点**:`QTextStream` 是文本流的方便接口,常与 `QFile` 结合;支持流操作符、字段填充、对齐与数字格式化;读写纯文本时更简洁。
+
+**详细解答**:
+
+```cpp
+/* 读取:用流读取全部文本 */
+file.open(QIODevice::ReadOnly | QIODevice::Text);
+QTextStream stream(&file);
+textEdit->setPlainText(stream.readAll());
+file.close();
+
+/* 写入:用流插入运算符 */
+file.open(QIODevice::WriteOnly | QIODevice::Text);
+QTextStream out(&file);
+out << textEdit->toPlainText();
+file.close();
+```
+
+`QFile` 更底层(`read/write/readLine` 字节级),`QTextStream` 面向文本,提供 `<<`/`>>`、`readLine()`、编码与格式化选项。需要逐行处理或格式化输出时优先用它。
+
+**追问**
+1. `QTextStream` 能操作 `QString`/`QByteArray` 吗?(能,它可在 QIODevice、QByteArray、QString 上操作)
+2. 读一行用什么?(`stream.readLine()`)
+
+### Q6.3 Qt 绘图系统由哪些类组成?`paintEvent()` 的作用是什么?何时会触发?
+
+**答案要点**:`QPainter`(绘制)、`QPaintDevice`(绘图设备基类)、`QPaintEngine`(平台抽象);`paintEvent()` 是 QWidget 的受保护虚函数,界面初始化或需要刷新时执行,用 `update()` 主动触发。
+
+**详细解答**:
+
+```cpp
+void Widget::paintEvent(QPaintEvent *)
+{
+    QPainter painter(this);   /* this 是绘图设备 */
+    painter.setRenderHints(QPainter::Antialiasing
+                           | QPainter::SmoothPixmapTransform);
+    painter.drawRect(10, 10, 100, 50);
+}
+```
+
+- `QWidget`、`QPixmap`、`QImage`、`QLabel` 等可视类都可作为“画布”。
+- `paintEvent()` 在父类中定义为空,专供子类画图;构造对象时会执行一次,之后要靠 `update()` 触发重绘。
+
+**追问**
+1. 为什么动画要用定时器 + `update()`?(`paintEvent` 不会自动反复执行)
+2. 抗锯齿怎么开?(`setRenderHints(QPainter::Antialiasing, true)`)
+
+### Q6.4 如何用 `QPainter` 绕图片中心旋转?变换顺序为什么重要?
+
+**答案要点**:先 `translate` 把原点移到图形中心,再 `rotate` 旋转,然后把原点恢复;因为绘图变换会累积,顺序错了旋转中心就不对。
+
+**详细解答**:
+
+```cpp
+QPainter painter(this);
+QPixmap image; image.load(":/image/cd.png");
+QRectF rect((width()-image.width())/2, (height()-image.height())/2,
+            image.width(), image.height());
+
+/* 1. 参考点从左上角移到 CD 中心 */
+painter.translate(rect.x() + rect.width()/2, rect.y() + rect.height()/2);
+/* 2. 按角度旋转 */
+painter.rotate(angle);
+/* 3. 参考点移回原点位置 */
+painter.translate(-(rect.x() + rect.width()/2), -(rect.y() + rect.height()/2));
+
+painter.drawImage(rect, image.toImage(), image.rect());
+painter.drawRect(rect.toRect());
+```
+
+配合 `QTimer` 每 100ms `update()` 一次,`angle++` 到 360 归零,CD 就转起来了。
+
+**追问**
+1. `QRectF` 与 `QRect` 的区别?(前者浮点坐标,绘图更精确)
+2. 如何只旋转某次绘制而不影响后续?(`painter.save()` / `painter.restore()`)
+
+### Q6.5 Qt Charts 由哪些类组成?画动态曲线的原理是什么?
+
+**答案要点**:`QChart`(图表)、`QChartView`(视图)、`QSplineSeries/QLineSeries`(曲线)、`QValueAxis`(坐标轴);动态曲线的原理是“存有限个数据点,超出则删队首,反复重绘”。
+
+**详细解答**:`.pro` 加 `QT += charts`,头文件加 `QT_CHARTS_USE_NAMESPACE`(或 `using namespace QtCharts`)。基本流程:创建 `QSplineSeries` → 创建 `QChart` 并 `addSeries` → 创建两条 `QValueAxis` 并 `addAxis` → `series->attachAxis()` → `chartView->setChart(chart)`。
+
+```cpp
+QList<int> data;
+void MainWindow::receivedData(int value)
+{
+    data.append(value);
+    while (data.size() > maxSize)     /* 超过上限删掉最早的 */
+        data.removeFirst();
+
+    splineSeries->clear();             /* 清空后重新追加,就形成“移动”效果 */
+    int xSpace = maxX / (maxSize - 1);
+    for (int i = 0; i < data.size(); ++i)
+        splineSeries->append(xSpace * i, data.at(i));
+}
+```
+
+**追问**
+1. 折线图用什么类?(`QLineSeries`)
+2. 坐标轴如何绑定曲线?(`splineSeries->attachAxis(axisX/axisY)`)
+
+---
+
+## 七、多线程与网络(5 题)
+
+### Q7.1 为什么要使用多线程?Qt 创建线程有哪两种方式?官方推荐哪种?
+
+**答案要点**:耗时操作会阻塞主线程导致界面冻结,需放到子线程;方式是继承 `QThread` 重写 `run()`,或继承 `QObject` 再用 `moveToThread()`;Qt4.8 后官方推荐后者,更灵活。
+
+**详细解答**:嵌入式 CPU 主频低,摄像头拍照、图像处理、文件读写都可能卡界面。两种方式区别:
+
+| 方式 | 新线程中执行的代码 | 灵活度 |
+| ---- | ------------------ | ------ |
+| 继承 QThread | 只有 `run()` 在新线程,其它方法仍在主线程 | 一般 |
+| 继承 QObject + moveToThread | 整个对象的方法(槽函数)都在新线程 | 更灵活 |
+
+**追问**
+1. 继承 QThread 时哪部分在新线程?(只有重写的 `run()`)
+2. 如何避免忘记解锁?(用 `QMutexLocker`,作用域结束自动解锁)
+
+### Q7.2 继承 `QThread` 的线程如何写?如何正确地启动和退出?
+
+**答案要点**:继承 `QThread` 重写 `run()`;用 `start()` 启动(它调用 `run()`),用 `quit()` + `wait()` 退出,不要用 `terminate()`。
+
+**详细解答**:
+
+```cpp
+class WorkerThread : public QThread {
+    Q_OBJECT
+public:
+    void run() override {
+        QString result = "线程开启成功";
+        sleep(2);                       /* 模拟耗时操作 */
+        emit resultReady(result);
+    }
+signals:
+    void resultReady(const QString &s);
+};
+
+/* 主线程 */
+workerThread = new WorkerThread(this);
+connect(workerThread, SIGNAL(resultReady(QString)), this, SLOT(handleResults(QString)));
+void MainWindow::pushButtonClicked() {
+    if (!workerThread->isRunning())     /* 防止重复启动 */
+        workerThread->start();
+}
+MainWindow::~MainWindow() {
+    workerThread->quit();
+    if (workerThread->wait(2000))
+        qDebug() << "线程已经结束!" << endl;
+}
+```
+
+**追问**
+1. `start()` 与直接调用 `run()` 有何区别?(`start()` 创建新线程并调用 `run()`;直接调用等于在主线程执行)
+2. `wait()` 的作用?(阻塞等待线程结束,可带超时)
+
+### Q7.3 继承 `QObject` + `moveToThread()` 如何安全终止一个循环线程?
+
+**答案要点**:用 `moveToThread()` 迁移 Worker,用 `QMutexLocker` 保护一个 `bool` 标志位,主线程调用 `stopWork()` 置位,工作循环检测到后跳出。
+
+**详细解答**:
+
+```cpp
+class Worker : public QObject {
+    Q_OBJECT
+private:
+    QMutex lock;
+    bool isCanRun;
+public slots:
+    void doWork1(const QString &parameter) {
+        isCanRun = true;
+        while (1) {
+            {   /* 限定锁的作用范围 */
+                QMutexLocker locker(&lock);
+                if (!isCanRun) break;
+            }
+            QThread::sleep(2);
+            emit resultReady(parameter + "doWork1 函数");
+        }
+        emit resultReady("打断doWork1 函数");
+    }
+public:
+    void stopWork() {                      /* 注意:不能放在槽函数下 */
+        QMutexLocker locker(&lock);
+        isCanRun = false;
+    }
+signals:
+    void resultReady(const QString &result);
+};
+
+/* 使用 */
+worker = new Worker;                       /* 继承 QObject 的多线程类不能指定父对象 */
+worker->moveToThread(&workerThread);
+connect(&workerThread, SIGNAL(finished()), worker, SLOT(deleteLater()));
+connect(this, SIGNAL(startWork(QString)), worker, SLOT(doWork1(QString)));
+```
+
+`quit()/exit()` 不会中途终止线程,`terminate()` 不安全。加锁虽然损耗性能,但能避免访问冲突。
+
+**追问**
+1. 为什么 `stopWork()` 不能做成槽?(否则它会被排到新线程的事件队列里,无法及时打断)
+2. 线程结束后如何释放对象?(`deleteLater()` 在事件循环空闲时安全销毁)
+
+### Q7.4 Qt 网络编程有哪些类?如何获取本机网络信息?
+
+**答案要点**:低级类 `QTcpSocket/QTcpServer/QUdpSocket`,高级类 `QNetworkRequest/QNetworkReply/QNetworkAccessManager`;`QHostInfo` 查主机名,`QNetworkInterface` 列网卡 IP。
+
+**详细解答**:
+
+```cpp
+#include <QNetworkInterface>
+#include <QHostInfo>
+
+QString str = "主机名称:" + QHostInfo::localHostName() + "\n";
+QList<QNetworkInterface> list = QNetworkInterface::allInterfaces();
+foreach (QNetworkInterface interface, list) {
+    /* 每个接口可能有 IPv4 / IPv6 两个地址 */
+    QList<QNetworkAddressEntry> entries = interface.addressEntries();
+    foreach (QNetworkAddressEntry entry, entries) {
+        str += entry.ip().toString() + "\n";
+    }
+}
+```
+
+使用网络需在 `.pro` 加 `QT += network`(嵌入式视频监控、云端 API 都用到)。
+
+**追问**
+1. `QHostInfo::fromName()` 与 `localHostName()` 区别?(前者按名字解析地址,后者取本机名)
+2. 访问一个 URL 用哪个类?(`QNetworkAccessManager::get/post`)
+
+### Q7.5 用 Qt 写 TCP 服务端/客户端、UDP 单播/广播/组播的关键接口分别是什么?
+
+**答案要点**:TCP 服务端 `QTcpServer::listen()` + `nextPendingConnection()`;客户端 `QTcpSocket::connectToHost()`;UDP `QUdpSocket::bind()` + `writeDatagram()/readDatagram()`;组播再 `joinMulticastGroup()`。
+
+**详细解答**:
+
+```cpp
+/* TCP 服务端 */
+tcpServer = new QTcpServer(this);
+connect(tcpServer, SIGNAL(newConnection()), this, SLOT(onNewConnection()));
+tcpServer->listen(QHostAddress::Any, 8888);
+void MainWindow::onNewConnection() {
+    tcpSocket = tcpServer->nextPendingConnection();
+    connect(tcpSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
+}
+
+/* TCP 客户端 */
+tcpSocket = new QTcpSocket(this);
+tcpSocket->connectToHost("192.168.1.100", 8888);
+
+/* UDP:发送数据报,无需先 bind */
+QUdpSocket udpSocket;
+QByteArray data = "hello";
+udpSocket.writeDatagram(data.data(), data.size(),
+                        QHostAddress::Broadcast, 8888);   /* 广播 */
+
+/* UDP 接收端 */
+udpSocket.bind(QHostAddress::Any, 8888);
+connect(&udpSocket, SIGNAL(readyRead()), this, SLOT(onReadyRead()));
+/* 收到后 readDatagram() 读出,注意必须先读走,否则收不到下一个数据报 */
+```
+
+**追问**
+1. UDP 单播与广播的区别?(单播发给指定 IP,广播发给同一网段所有主机)
+2. 组播要额外做什么?(接收端 `joinMulticastGroup()` 加入组播组)
+
+---
+
+## 八、多媒体、数据库与嵌入式Qt(5 题)
+
+### Q8.1 Qt 多媒体模块有哪些主要类?`QSound` 与 `QSoundEffect` 有什么区别?
+
+**答案要点**:`QMediaPlayer`、`QSound`、`QSoundEffect`、`QAudioOutput`、`QAudioInput`、`QAudioRecorder`、`QVideoWidget`;`QSound` 只能异步播放本地 WAV,`QSoundEffect` 还能播放网络文件。
+
+**详细解答**:Qt5 起弃用 phonon,改用 Qt Multimedia 模块,`pro` 加 `QT += multimedia`。Ubuntu 需安装 GStreamer 解码插件,Windows 需安装 LAVFilters。
+
+| 类 | 播放源 | 特点 |
+| -- | ------ | ---- |
+| QSound | 本地 WAV | 异步、低延时、体积大,适合短提示音 |
+| QSoundEffect | 本地或网络 | 支持 `QUrl` 网络文件 |
+
+播放音效示例:`QSound::play(":/audio/bell.wav");`
+
+**追问**
+1. 嵌入式为什么常用 WAV 做按键音?(低延时、无解码负担)
+2. 播放器为何要装解码插件?(Qt 只提供接口,实际解码由平台插件完成)
+
+### Q8.2 `QMediaPlayer` 与 `QAudioRecorder` 分别怎么用?
+
+**答案要点**:`QMediaPlayer` 设置媒体源并 `play()`,视频配合 `QVideoWidget::setMediaPlayer()` 显示;`QAudioRecorder` 设置输入设备、编码参数、容器格式与输出路径后 `record()/stop()`。
+
+**详细解答**:
+
+```cpp
+/* 播放器(音乐 / 视频) */
+QMediaPlayer *player = new QMediaPlayer;
+player->setMedia(QUrl::fromLocalFile("test.mp4"));
+videoWidget->setMediaPlayer(player);     /* 视频画面输出到 QVideoWidget */
+player->play();
+
+/* 录音(语音识别项目用它录 16k 单声道 wav) */
+QAudioRecorder *recorder = new QAudioRecorder(this);
+QAudioEncoderSettings settings;
+settings.setSampleRate(16000);           /* 百度语音要求 */
+settings.setChannelCount(1);             /* 单声道 */
+recorder->setEncodingSettings(settings, QVideoEncoderSettings(), "audio/x-wav");
+recorder->setOutputLocation(QUrl::fromLocalFile("./16k.wav"));
+recorder->record();                      /* 开始录音 */
+recorder->stop();                        /* 停止录音 */
+```
+
+播放器还可通过 `QMediaPlaylist` 管理多首歌曲,或用 `setPlaybackMode` 设置循环。
+
+**追问**
+1. 视频画面为什么不能直接贴到普通 `QLabel` 上?(需要 `QVideoWidget` 作为渲染目标)
+2. 录音保存 wav 要设置哪个属性?(容器格式设为 `audio/x-wav`)
+
+### Q8.3 Qt 如何操作数据库?`QSqlDatabase`、`QSqlQuery`、`QSqlTableModel` 各自的作用是什么?
+
+**答案要点**:`QSqlDatabase` 连接数据库,`QSqlQuery` 执行 SQL,`QSqlTableModel` 把表读成模型供 `QTableView` 显示;嵌入式常用 SQLite。
+
+**详细解答**:`pro` 加 `QT += core gui sql`。SQLite 是进程内、无服务器、零配置的轻量数据库,适合嵌入式。
+
+```cpp
+/* 打开 / 创建数据库 */
+QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
+db.setDatabaseName("alarm.db");
+if (!db.open()) qDebug() << db.lastError();
+
+/* 用 QSqlQuery 建表、增删改查 */
+QSqlQuery query(db);
+query.exec("create table alarm (id int primary key, time vchar(15), state vchar(10))");
+query.prepare("insert into alarm values(?, ?, ?)");   /* 预处理防注入 */
+query.addBindValue(0); query.addBindValue("06:00"); query.addBindValue("false");
+query.exec();
+
+/* 用 QSqlTableModel + QTableView 显示,可直接编辑单元格改库 */
+QSqlTableModel *model = new QSqlTableModel(this, db);
+model->setTable("alarm");
+model->setEditStrategy(QSqlTableModel::OnFieldChange);
+model->select();
+QTableView *view = new QTableView;
+view->setModel(model);
+```
+
+**追问**
+1. `QSqlQuery` 与 `QSqlTableModel` 各自适合什么场景?(前者适合精确 SQL 操作,后者适合表格化显示与编辑)
+2. 想只读展示查询结果用什么?(`QSqlQueryModel`)
+
+### Q8.4 嵌入式 Qt 如何交叉编译?运行时如何选择显示平台插件?
+
+**答案要点**:用交叉工具链的 `qmake` + `make`,或在 Qt Creator 配置 ARM Kit;运行时用 `-platform linuxfb` 或 `-platform eglfs` 指定平台插件。
+
+**详细解答**:
+
+```bash
+# 命令行交叉编译
+/opt/Qt5.12.9/Tools/QtCreator/bin/qtcreator.sh &   # 带环境变量启动 Qt Creator
+/path/to/arm-qmake 01_led.pro && make -j8
+scp 01_led root@192.168.1.100:/home/root/
+
+# 运行
+./01_led                      # 使用默认平台插件
+./01_led -platform eglfs      # EGL/OpenGL ES 全屏(有 GPU)
+./01_led -platform linuxfb    # Linux Framebuffer(无 GPU)
+```
+
+| 插件 | 依赖 | 场景 |
+| ---- | ---- | ---- |
+| linuxfb | `/dev/fb0` | 无 GPU、直接帧缓冲 |
+| eglfs | EGL/OpenGL ES | 有 GPU,I.MX6ULL 出厂系统常用 |
+| xcb | X Server | Ubuntu 桌面上仿真 |
+
+> ⚠️ **来源说明**:平台插件部分不属于《I.MX6U嵌入式Qt开发指南》内容,为扩展知识。
+
+**追问**
+1. 交叉编译后为什么常要拷贝可执行文件到板子运行?(远程运行时环境变量/路径可能不一致)
+2. 先在哪里验证界面逻辑再交叉编译?(Ubuntu 桌面套件仿真)
+
+### Q8.5 在嵌入式 Qt 中操作硬件(LED/传感器/摄像头)的统一模式是什么?举一个综合项目说明。
+
+**答案要点**:确认设备节点 → 用定时器/事件触发 → 用 QFile 或 C 的 `open/read` 读写 → 通过信号把数据交给界面;综合项目(如智能家居)再叠加串口、网络与云端 API。
+
+**详细解答**:Qt 在嵌入式里操作硬件的通用套路:
+
+1. **找节点**:LED `.../sys-led/brightness`、AP3216C `/sys/class/misc/ap3216c/*`、ICM20608 `/dev/icm20608`、摄像头 `/dev/video1`、串口 `/dev/ttymxc2`、CAN `can0`。
+2. **定触发**:低速量用 `QTimer`(如 500ms 采一次),交互用事件(`keyPressEvent`)或信号(`readyRead`、`framesReceived`)。
+3. **读写数据**:QFile(sysfs 文本)或 C 的 `open()/read()`(二进制结构体,如 icm20608 的 7 个 int)。
+4. **推给界面**:用自定义信号(如 `ap3216cDataChanged()`)通知主线程刷新。
+
+综合实例(智能家居物联网):ESP8266 通过串口用 AT 指令接入 WIFI 与原子云 → 定时发心跳;控制端用 `QNetworkAccessManager` 走 HTTPS 拿设备列表与 `token`,用 `QWebSocket` 鉴权并订阅消息;点开关时下发 `0x03` 命令,云端转发到设备,设备端收到“开/关”后写 LED 节点。整个链路把**串口 + 网络 + 硬件控制 + 信号槽 + 多模块协作**都串了起来。
+
+**追问**
+1. 摄像头/视频监控为什么必须开子线程?(采集与编码耗时会卡主线程)
+2. 为什么 Qt 蓝牙只能在 Linux/BlueZ 上跑?(依赖 BlueZ 协议栈,Windows 需自行移植)
+
+---
+
+**内容来源**:正点原子《I.MX6U嵌入式Qt开发指南V1.2》第一章 在Ubuntu下编写C++、第二章 C++基础、第三章 初识Qt、第四章 使用Qt Designer开发、第五章 Qt信号与槽、第六章 Qt Creator的使用技巧、第七章 Qt控件、第八章 文本读写、第九章 绘图与图表、第十章 多线程、第十一章 网络编程、第十二章 多媒体、第十三章 数据库、第十四章 I.MX6U Qt开发 ~ 第二十八章 视频监控项目;配套例程 `Embedded-Qt-Tutorial/Qt/01`、`Qt/02`、`Qt/03`、`Qt/04`;平台插件 `-platform linuxfb/-platform eglfs` 部分为扩展知识。