# Day 3 · 高级控件 Model/View / 事件机制 / 文件IO与JSON

# Day 3

### 高级控件 Model/View / 事件机制 / 文件 IO 与 JSON

C++/Qt 方向实训 · 第 3 / 5 天 · 讲:练 ≈ 1:2.7

- 模块 A（3 课时）：Model/View 架构 / 事件机制
- 模块 B（3 课时）：文件 IO / JSON 解析 → 本地数据存储案例

模块 A · 讲授 35min

## Model/View 架构 / 事件机制 讲

### Model/View：数据与展示分离 讲

#### 核心概念

- Model/View 把"数据"和"怎么展示数据"彻底拆开：模型（`QAbstractItemModel` 及其子类）只负责存数据、回答"这个位置是什么"，视图（`QListView`/ `QTableView`/...）只负责画——同一份模型可以同时喂给多个不同视图。
- 模型用统一的 `QModelIndex`（行/列/父项）寻址数据，视图向模型要数据时 还要带上"角色"（`Qt::DisplayRole` 显示文本、`Qt::DecorationRole` 图标……），同一个位置可以按不同角色返回不同内容。

#### 常见的坑

- 直接改模型内部数据却不发信号（`dataChanged`/`beginInsertRows`+ `endInsertRows`），视图不会自动刷新，界面和数据"对不上"。
- 用了简单的 `QStandardItemModel` 却又想自定义复杂数据结构：应该继承 `QAbstractItemModel` 自己实现，而不是硬塞进 Item 模型。

### Model/View 模型视图架构（教学补充） 讲

    // 模型：只管数据
    auto* model = new QStandardItemModel(this);
    model->appendRow(new QStandardItem(QStringLiteral("需求分析")));

    // 视图：只管展示，不持有数据
    auto* view = new QListView(this);
    view->setModel(model);

    // 选中状态变化 → 通知外部，模型和视图之间没有直接耦合
    connect(view->selectionModel(), &QItemSelectionModel::currentChanged, ...);

出处：docs/teaching/examples/model_view/qt_model_view_basics.cpp （配套 README）—— 仓库 `p03/` 的 wiki 抓取内容没有 Model/View，这是本轮补写的教学补充示例，已离屏验证跑通。

### 事件循环：事件 vs 信号 讲

#### 核心概念

- `app.exec()` 进入的就是事件循环（复用 Day2 概念）——鼠标点击、 键盘输入、定时器触发都会变成"事件"（`QEvent`），被塞进这个循环里依次分发给 对应控件。
- 事件和信号是两条不同的机制：事件是"操作系统/框架推给控件"的通知，走 `event()`/`xxxEvent()` 虚函数或事件过滤器；信号槽是"对象之间" 主动通知，走 `connect()`。控件收到事件后，常见做法就是在事件处理函数里 `emit` 一个信号，把"底层事件"转成"业务语义通知"。
- 今天讲的"事件过滤器"就是在事件被分发给控件之前，插一个"拦截点"。

#### 常见的坑

- 在事件循环所在的主线程做耗时操作会直接卡住整个界面（无法重绘、按钮点了没反应）—— 这也是 Day4 要讲 QThread 的原因。

### 事件循环机制（复用 Day2 概念） 讲

`app.exec()` 进入的就是事件循环——鼠标点击、键盘输入、定时器 触发都会变成事件，被塞进这个循环里依次分发。今天讲的"事件过滤器"就是在 事件被分发给控件之前，插一个"拦截点"。

出处：p03/ch02/minimal_qt_app.cpp

### 自定义事件处理：核心概念 讲

#### 核心概念

- 拦截事件有两条路：重写 `xxxEvent()`（只影响这一个控件自己）或装事件过滤器 `installEventFilter()`（可以在事件到达目标控件之前，由另一个对象统一拦截， 常用于"一个地方管多个控件"的场景）。
- 事件对象有 `accept()`/`ignore()` 状态：处理完事件要表明"我 吃掉了这个事件"还是"继续往上传"，决定事件是否会继续传播给父控件。

#### 常见的坑

- 重写 `xxxEvent()` 时忘记调用基类实现（如 `QWidget::mouseMoveEvent`）， 可能丢失控件本身需要的默认行为。
- 事件过滤器安装到一个对象上后，如果被观察对象先于过滤器对象销毁（或反过来），要注意 生命周期——Qt 会在对象销毁时自动清理，但过滤逻辑里持有的裸指针不会自动清空。

### 自定义事件处理 / 事件过滤器 讲

    class EventLabel : public QLabel {
        Q_OBJECT
    public:
        explicit EventLabel(QWidget* p=nullptr):QLabel(p){ setMouseTracking(true); }
    protected:
        void mouseMoveEvent(QMouseEvent* event) override {
            setText(QString("Move: (%1, %2)").arg(event->x()).arg(event->y()));
        }
    };

重写 `xxxEvent` 是 Qt 事件处理的主线之一（另一条线是事件过滤器 `installEventFilter`）。出处：p03/ch09/mouse_event.cpp； 事件过滤器见 event_filter_mainwindow.cpp

模块 A · 练习 95min

## 动手：Model/View + 事件 练

#### 任务卡 1 · Model/View 换视图 练

目标
  把 `qt_model_view_basics.cpp` 里的 `QListView` 换成 `QTableView`，模型代码不改
验收标准
  编译通过，界面正常展示同一份数据，证明"同一个模型可以喂给不同视图"
基础知识 · 参考
  qt_model_view_basics.cpp · README

#### 任务卡 2 · 事件过滤器实操 练

目标
  仿 `event_filter_mainwindow.cpp`，给一个控件装事件过滤器，拦截鼠标按下事件并打印坐标
验收标准
  点击控件后终端/调试输出能看到坐标信息
基础知识 · 参考
  event_filter_mainwindow.cpp · mouse_event.cpp

模块 B · 讲授 35min

## 文件 IO / JSON 解析 讲

### QIODevice：统一的读写抽象 讲

#### 核心概念

- `QFile`/`QTextStream`/`QDataStream` 都建立在 `QIODevice` 这一统一读写接口上——同样的"打开/读/写/关闭"套路，以后遇到网络 套接字/串口一类设备也是同一套接口，学一次可以迁移到别的设备类型。
- `QTextStream` 面向"文本行"（人可读），`QDataStream` 面向 "结构化二进制"（保留类型信息，程序间高效互传但人不可直接读）——按需求选，不是谁更好。

#### 常见的坑

- 忘记检查 `open()` 的返回值：文件不存在/无权限时静默失败，后续读写在一个 没打开的设备上操作，容易读出空数据而不报错。
- 混淆文本模式和二进制模式（`QIODevice::Text`）：文本模式下 Qt 会做换行符 转换，用来读写二进制数据会破坏内容。

### 文件系统：QFile 读文本 讲

    QFile file("in.txt");
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "打开失败"; return -1; }
    while (!file.atEnd()) { qDebug() << file.readLine(); }

出处：p03/ch11/qfile_basic.cpp；结构化读写见 qtextstream_io.cpp、 qdatastream_io.cpp

### JSON：序列化与反序列化的往返 讲

#### 核心概念

- JSON 读写的本质是"内存对象 ↔ 文本"的一对互逆操作：序列化（`QJsonDocument::toJson`） 把内存数据变成可存/可传的文本，反序列化（`fromJson`）再变回来——正确性的标准是 "写出去再读回来，数据要一致"（round-trip）。
- `QJsonValue` 是一个变体（variant）类型，能装字符串/数字/布尔/数组/对象， 取值时要按预期类型调用 `toString()`/`toInt()` 等。

#### 常见的坑

- 忘记检查 `QJsonDocument::fromJson` 的 `QJsonParseError`： 文本格式错误时返回一个空文档，后续取值全是默认值，不会崩溃也不会报错，很难定位。
- 类型不匹配时 `toInt()`/`toString()` 等不会抛异常，只会静默 返回默认值（0/空串），排查时容易误以为"数据没写进去"。

### JSON 解析与写入（教学补充） 讲

    QJsonObject student;
    student["name"] = QStringLiteral("张三");
    student["score"] = 92;

    QJsonDocument doc(student);
    QByteArray bytes = doc.toJson(QJsonDocument::Indented);   // 序列化

    QJsonDocument parsed = QJsonDocument::fromJson(bytes);     // 反序列化
    QJsonObject obj = parsed.object();
    qDebug() << obj["name"].toString() << obj["score"].toInt();

出处：docs/teaching/examples/json_io/qt_json_parse_write.cpp （配套 README）—— 仓库 `p03/ch11` 的 wiki 抓取内容只有 QFile/QTextStream/QDataStream，没有 JSON， 这是本轮补写的教学补充示例；不需要 GUI，`tools/run_qt.sh qt_json_parse_write` 直接跑即可，不需要 `QT_QPA_PLATFORM=offscreen`。

模块 B · 练习 95min（本地数据存储案例，练习任务，非预建方案）

## 动手：本地数据存储小工具 练

#### 任务卡 1 · JSON 落盘 练

目标
  把 `qt_json_parse_write.cpp` 的 JSON 数据实际写入一个文件（结合 `qfile_basic.cpp` 的 `QFile` 写法），再读回来解析校验
验收标准
  程序运行后磁盘上生成一个 `.json` 文件，内容可读；重新运行程序能正确解析出 之前写入的数据
基础知识 · 参考
  qt_json_parse_write.cpp · README · qfile_basic.cpp

#### 任务卡 2 · 本地数据存储小工具 练

目标
  设计一个"学员信息"结构（姓名/成绩/标签），支持增加一条记录并追加保存到 JSON 文件， 重启程序后能读出之前保存的全部记录
验收标准
  至少能存 2 条以上记录并在重启后完整读回；数据字段不丢失
基础知识 · 参考
  docs/teaching/examples/json_io/ · qtextstream_io.cpp

## 当日产出验证

#### 现场演示

现场演示本地数据存储小工具，验证"写入 → 重启 → 读回"完整闭环。

返回目录 · ← 上一天 · 下一天 →