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.cppqdatastream_io.cpp

JSON:序列化与反序列化的往返

核心概念

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

常见的坑

  • 忘记检查 QJsonDocument::fromJsonQJsonParseError: 文本格式错误时返回一个空文档,后续取值全是默认值,不会崩溃也不会报错,很难定位。
  • 类型不匹配时 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.cppQFile 写法),再读回来解析校验
验收标准
程序运行后磁盘上生成一个 .json 文件,内容可读;重新运行程序能正确解析出 之前写入的数据
基础知识 · 参考
qt_json_parse_write.cpp · README · qfile_basic.cpp

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

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

当日产出验证

现场演示

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

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