a6743fcea0
此前一版「讲」部分基本只是代码演示(贴代码 + 一句出处说明),没有讲透 知识点本身的原理。改为覆盖清单原则:每个知识点在大纲里至少固定两项—— 核心概念(是什么/为什么,不只是怎么写)+ 常见的坑(注意事项/误区), 课堂实际讲解深度按学员反馈现场调整,但大纲必须列全,以免漏讲或退化成 纯代码演示。 - custom.css 新增 .concept-box/.pitfall-box 卡片样式 - day1–5.html 每个知识点前插入对应卡片,内容与 OUTLINE.md 的覆盖清单一一对应
347 lines
17 KiB
HTML
347 lines
17 KiB
HTML
<!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>模块 A(3 课时):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>
|