Files
scuc-qt-course/docs/teaching/slides/day3.html
T
张宗平 a6743fcea0 每个「讲」知识点补齐核心概念 + 常见的坑,幻灯片与大纲同步
此前一版「讲」部分基本只是代码演示(贴代码 + 一句出处说明),没有讲透
知识点本身的原理。改为覆盖清单原则:每个知识点在大纲里至少固定两项——
核心概念(是什么/为什么,不只是怎么写)+ 常见的坑(注意事项/误区),
课堂实际讲解深度按学员反馈现场调整,但大纲必须列全,以免漏讲或退化成
纯代码演示。

- custom.css 新增 .concept-box/.pitfall-box 卡片样式
- day1–5.html 每个知识点前插入对应卡片,内容与 OUTLINE.md 的覆盖清单一一对应
2026-07-02 16:25:46 +08:00

347 lines
17 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 3 · 高级控件 Model/View / 事件机制 / 文件IO与JSON</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="reveal/reveal.css">
<link rel="stylesheet" href="reveal/theme.css">
<link rel="stylesheet" href="reveal/plugin/highlight/monokai.css">
<link rel="stylesheet" href="custom.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>Day 3</h1>
<h3>高级控件 Model/View / 事件机制 / 文件 IO 与 JSON</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 3 / 5 天 · 讲:练 ≈ 1:2.7</p>
<ul>
<li>模块 A3 课时):Model/View 架构 / 事件机制</li>
<li>模块 B(3 课时):文件 IO / JSON 解析 → 本地数据存储案例</li>
</ul>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>Model/View 架构 / 事件机制 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>Model/View:数据与展示分离 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>Model/View 把"数据"和"怎么展示数据"彻底拆开:模型(<code>QAbstractItemModel</code>
及其子类)只负责存数据、回答"这个位置是什么",视图(<code>QListView</code>/
<code>QTableView</code>/...)只负责画——同一份模型可以同时喂给多个不同视图。</li>
<li>模型用统一的 <code>QModelIndex</code>(行/列/父项)寻址数据,视图向模型要数据时
还要带上"角色"<code>Qt::DisplayRole</code> 显示文本、<code>Qt::DecorationRole</code>
图标……),同一个位置可以按不同角色返回不同内容。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>直接改模型内部数据却不发信号(<code>dataChanged</code>/<code>beginInsertRows</code>+
<code>endInsertRows</code>),视图不会自动刷新,界面和数据"对不上"。</li>
<li>用了简单的 <code>QStandardItemModel</code> 却又想自定义复杂数据结构:应该继承
<code>QAbstractItemModel</code> 自己实现,而不是硬塞进 Item 模型。</li>
</ul>
</div>
</section>
<section>
<h3>Model/View 模型视图架构(教学补充) <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
// 模型:只管数据
auto* model = new QStandardItemModel(this);
model->appendRow(new QStandardItem(QStringLiteral("需求分析")));
// 视图:只管展示,不持有数据
auto* view = new QListView(this);
view->setModel(model);
// 选中状态变化 → 通知外部,模型和视图之间没有直接耦合
connect(view->selectionModel(), &QItemSelectionModel::currentChanged, ...);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../examples/model_view/qt_model_view_basics.cpp">docs/teaching/examples/model_view/qt_model_view_basics.cpp</a>
<a class="filelink" target="_blank" href="../examples/model_view/README.md">配套 README</a>)——
仓库 <code>p03/</code> 的 wiki 抓取内容没有 Model/View,这是本轮补写的教学补充示例,已离屏验证跑通。</small></p>
</section>
<section>
<h3>事件循环:事件 vs 信号 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><code>app.exec()</code> 进入的就是事件循环(复用
<a class="filelink" target="_blank" href="day2.html">Day2</a> 概念)——鼠标点击、
键盘输入、定时器触发都会变成"事件"(<code>QEvent</code>),被塞进这个循环里依次分发给
对应控件。</li>
<li>事件和信号是两条不同的机制:事件是"操作系统/框架推给控件"的通知,走
<code>event()</code>/<code>xxxEvent()</code> 虚函数或事件过滤器;信号槽是"对象之间"
主动通知,走 <code>connect()</code>。控件收到事件后,常见做法就是在事件处理函数里
<code>emit</code> 一个信号,把"底层事件"转成"业务语义通知"。</li>
<li>今天讲的"事件过滤器"就是在事件被分发给控件之前,插一个"拦截点"。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>在事件循环所在的主线程做耗时操作会直接卡住整个界面(无法重绘、按钮点了没反应)——
这也是 <a class="filelink" target="_blank" href="day4.html">Day4</a> 要讲 QThread 的原因。</li>
</ul>
</div>
</section>
<section>
<h3>事件循环机制(复用 Day2 概念) <span class="tag tag-lecture"></span></h3>
<p><code>app.exec()</code> 进入的就是事件循环——鼠标点击、键盘输入、定时器
触发都会变成事件,被塞进这个循环里依次分发。今天讲的"事件过滤器"就是在
事件被分发给控件之前,插一个"拦截点"。</p>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch02/minimal_qt_app.cpp">p03/ch02/minimal_qt_app.cpp</a></small></p>
</section>
<section>
<h3>自定义事件处理:核心概念 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>拦截事件有两条路:重写 <code>xxxEvent()</code>(只影响这一个控件自己)或装事件过滤器
<code>installEventFilter()</code>(可以在事件到达目标控件之前,由另一个对象统一拦截,
常用于"一个地方管多个控件"的场景)。</li>
<li>事件对象有 <code>accept()</code>/<code>ignore()</code> 状态:处理完事件要表明"我
吃掉了这个事件"还是"继续往上传",决定事件是否会继续传播给父控件。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>重写 <code>xxxEvent()</code> 时忘记调用基类实现(如 <code>QWidget::mouseMoveEvent</code>),
可能丢失控件本身需要的默认行为。</li>
<li>事件过滤器安装到一个对象上后,如果被观察对象先于过滤器对象销毁(或反过来),要注意
生命周期——Qt 会在对象销毁时自动清理,但过滤逻辑里持有的裸指针不会自动清空。</li>
</ul>
</div>
</section>
<section>
<h3>自定义事件处理 / 事件过滤器 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
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()));
}
};
</code></pre>
<p><small>重写 <code>xxxEvent</code> 是 Qt 事件处理的主线之一(另一条线是事件过滤器
<code>installEventFilter</code>)。出处:<a class="filelink" target="_blank"
href="../../../p03/ch09/mouse_event.cpp">p03/ch09/mouse_event.cpp</a>
事件过滤器见 <a class="filelink" target="_blank" href="../../../p03/ch09/event_filter_mainwindow.cpp">event_filter_mainwindow.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:Model/View + 事件 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · Model/View 换视图 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd><code>qt_model_view_basics.cpp</code> 里的 <code>QListView</code> 换成
<code>QTableView</code>,模型代码不改</dd>
<dt>验收标准</dt>
<dd>编译通过,界面正常展示同一份数据,证明"同一个模型可以喂给不同视图"</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../examples/model_view/qt_model_view_basics.cpp">qt_model_view_basics.cpp</a> ·
<a class="filelink" target="_blank" href="../examples/model_view/README.md">README</a></dd>
</dl>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 事件过滤器实操 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>仿 <code>event_filter_mainwindow.cpp</code>,给一个控件装事件过滤器,拦截鼠标按下事件并打印坐标</dd>
<dt>验收标准</dt>
<dd>点击控件后终端/调试输出能看到坐标信息</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch09/event_filter_mainwindow.cpp">event_filter_mainwindow.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch09/mouse_event.cpp">mouse_event.cpp</a></dd>
</dl>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>文件 IO / JSON 解析 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>QIODevice:统一的读写抽象 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><code>QFile</code>/<code>QTextStream</code>/<code>QDataStream</code> 都建立在
<code>QIODevice</code> 这一统一读写接口上——同样的"打开/读/写/关闭"套路,以后遇到网络
套接字/串口一类设备也是同一套接口,学一次可以迁移到别的设备类型。</li>
<li><code>QTextStream</code> 面向"文本行"(人可读),<code>QDataStream</code> 面向
"结构化二进制"(保留类型信息,程序间高效互传但人不可直接读)——按需求选,不是谁更好。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>忘记检查 <code>open()</code> 的返回值:文件不存在/无权限时静默失败,后续读写在一个
没打开的设备上操作,容易读出空数据而不报错。</li>
<li>混淆文本模式和二进制模式(<code>QIODevice::Text</code>):文本模式下 Qt 会做换行符
转换,用来读写二进制数据会破坏内容。</li>
</ul>
</div>
</section>
<section>
<h3>文件系统:QFile 读文本 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
QFile file("in.txt");
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "打开失败"; return -1; }
while (!file.atEnd()) { qDebug() << file.readLine(); }
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch11/qfile_basic.cpp">p03/ch11/qfile_basic.cpp</a>;结构化读写见
<a class="filelink" target="_blank" href="../../../p03/ch11/qtextstream_io.cpp">qtextstream_io.cpp</a>
<a class="filelink" target="_blank" href="../../../p03/ch11/qdatastream_io.cpp">qdatastream_io.cpp</a></small></p>
</section>
<section>
<h3>JSON:序列化与反序列化的往返 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>JSON 读写的本质是"内存对象 ↔ 文本"的一对互逆操作:序列化(<code>QJsonDocument::toJson</code>
把内存数据变成可存/可传的文本,反序列化(<code>fromJson</code>)再变回来——正确性的标准是
"写出去再读回来,数据要一致"round-trip)。</li>
<li><code>QJsonValue</code> 是一个变体(variant)类型,能装字符串/数字/布尔/数组/对象,
取值时要按预期类型调用 <code>toString()</code>/<code>toInt()</code> 等。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>忘记检查 <code>QJsonDocument::fromJson</code><code>QJsonParseError</code>
文本格式错误时返回一个空文档,后续取值全是默认值,不会崩溃也不会报错,很难定位。</li>
<li>类型不匹配时 <code>toInt()</code>/<code>toString()</code> 等不会抛异常,只会静默
返回默认值(0/空串),排查时容易误以为"数据没写进去"。</li>
</ul>
</div>
</section>
<section>
<h3>JSON 解析与写入(教学补充) <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
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();
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../examples/json_io/qt_json_parse_write.cpp">docs/teaching/examples/json_io/qt_json_parse_write.cpp</a>
<a class="filelink" target="_blank" href="../examples/json_io/README.md">配套 README</a>)——
仓库 <code>p03/ch11</code> 的 wiki 抓取内容只有 QFile/QTextStream/QDataStream,没有 JSON
这是本轮补写的教学补充示例;不需要 GUI,<code>tools/run_qt.sh qt_json_parse_write</code>
直接跑即可,不需要 <code>QT_QPA_PLATFORM=offscreen</code></small></p>
</section>
</section>
<!-- ===================== 模块 B:练(本地数据存储案例) ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min(本地数据存储案例,练习任务,非预建方案)</p>
<h2>动手:本地数据存储小工具 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · JSON 落盘 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd><code>qt_json_parse_write.cpp</code> 的 JSON 数据实际写入一个文件(结合
<code>qfile_basic.cpp</code><code>QFile</code> 写法),再读回来解析校验</dd>
<dt>验收标准</dt>
<dd>程序运行后磁盘上生成一个 <code>.json</code> 文件,内容可读;重新运行程序能正确解析出
之前写入的数据</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../examples/json_io/qt_json_parse_write.cpp">qt_json_parse_write.cpp</a> ·
<a class="filelink" target="_blank" href="../examples/json_io/README.md">README</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch11/qfile_basic.cpp">qfile_basic.cpp</a></dd>
</dl>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 本地数据存储小工具 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>设计一个"学员信息"结构(姓名/成绩/标签),支持增加一条记录并追加保存到 JSON 文件,
重启程序后能读出之前保存的全部记录</dd>
<dt>验收标准</dt>
<dd>至少能存 2 条以上记录并在重启后完整读回;数据字段不丢失</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../examples/json_io/">docs/teaching/examples/json_io/</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch11/qtextstream_io.cpp">qtextstream_io.cpp</a></dd>
</dl>
</div>
</section>
</section>
<!-- ===================== 收尾 ===================== -->
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>现场演示</h4>
<p>现场演示本地数据存储小工具,验证"写入 → 重启 → 读回"完整闭环。</p>
</div>
<p class="footer-note"><small>明天 Day4:2D 图形绘制 / 多线程 / 动画框架与状态机 ——
大纲:<a class="filelink" href="../OUTLINE.md">docs/teaching/OUTLINE.md</a></small></p>
<p><a class="filelink" href="index.html">返回目录</a> ·
<a class="filelink" href="day2.html">← 上一天</a> ·
<a class="filelink" href="day4.html">下一天 →</a></p>
</section>
</div>
</div>
<script src="reveal/reveal.js"></script>
<script src="reveal/plugin/highlight/highlight.js"></script>
<script>
Reveal.initialize({
hash: true, controls: true, progress: true, center: true, slideNumber: true,
plugins: [ RevealHighlight ]
});
</script>
</body>
</html>