教案对齐官方方案书:重写为 5 天 C++/Qt 教学设计,补齐 6 个 Qt 技术缺口示例

- docs/teaching/OUTLINE.md、slides/day1-5.html、index.html:替换此前自定义的
  4 天方案,严格对齐 docs/TRAINING_PLAN_2026.md「C++方向」Day1-5 课堂内容
  (Day6-10 为纯项目实作,不在教案范围)
- docs/teaching/examples/:官方方案书要求但仓库 p03/(wiki 抓取内容) 未覆盖的
  QSS/Model-View/JSON/QThread/QPropertyAnimation/QStateMachine 六个技术点,
  各补一个最小可运行示例 + README 教学文档,均已离屏验证跑通;手工维护,
  不属于 gen_part3.py 生成产物
- 根 CMakeLists.txt:接入 docs/teaching/examples 构建(BUILD_QT_PART 分支内),
  不改动生成器管理的 p01/p03 CMakeLists
- 任务卡改为目标/验收标准/基础知识参考三段式模板,custom.css 配套新增样式
- docs/teaching/PRETEST.md:10 题 10 分钟 C++ 摸底测验,验证教案假定的受众
  基础是否成立,并与 OUTLINE.md §0 的分层预案挂钩
This commit is contained in:
张宗平
2026-07-02 11:31:44 +08:00
parent 6bb38563b5
commit 83620c0cf5
23 changed files with 1862 additions and 1041 deletions
+129 -160
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 3 · 泛型与标准库</title>
<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">
@@ -15,11 +15,11 @@
<section>
<h1>Day 3</h1>
<h3>泛型与标准库</h3>
<p class="module-badge">Qt 方向实训 · 第 3 / 4 天 · 讲:练 ≈ 1:2.7</p>
<h3>高级控件 Model/View / 事件机制 / 文件 IO 与 JSON</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 3 / 5 天 · 讲:练 ≈ 1:2.7</p>
<ul>
<li>模块 A3 课时):模板 + 类型转换</li>
<li>模块 B3 课时):异常 + IO 流 + STL</li>
<li>模块 A3 课时):Model/View 架构 / 事件机制</li>
<li>模块 B3 课时):文件 IO / JSON 解析 → 本地数据存储案例</li>
</ul>
</section>
@@ -27,49 +27,55 @@
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>模板 + 类型转换 <span class="tag tag-lecture"></span></h2>
<ul>
<li>函数模板 vs 类模板</li>
<li>模板的局限性</li>
<li>四种显式类型转换(static / dynamic / const_cast</li>
</ul>
<h2>Model/View 架构 / 事件机制 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>类模板:类型不能自动推导 <span class="tag tag-lecture"></span></h3>
<h3>Model/View 模型视图架构(教学补充) <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
template&lt;class NameType, class AgeType&gt;
class Person{
// 模型:只管数据
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>事件循环机制(复用 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>
<pre><code class="cpp" data-trim data-noescape>
class EventLabel : public QLabel {
Q_OBJECT
public:
Person(NameType name, AgeType age){ mName = name; mAge = age; }
void showPerson(){ cout &lt;&lt; mName &lt;&lt; " " &lt;&lt; mAge &lt;&lt; endl; }
NameType mName; AgeType mAge;
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()));
}
};
// Person P1("德玛西亚", 18); // 非法——类模板不支持类型自动推导
Person&lt;string, int&gt; P1("德玛西亚", 18); // 必须显式写出模板实参
</code></pre>
<p><small>与函数模板最大的区别:函数模板能从实参推导类型,类模板不能——这是常见误用点。
出处:<a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_basics.cpp">p01/ch05/class_template_basics.cpp</a></small></p>
</section>
<section>
<h3>dynamic_cast:运行期检查的向下转型 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Animal { public: virtual void ShowName() = 0; }; // 必须有虚函数(多态类型)
class Dog : public Animal { virtual void ShowName(){ /*...*/ } };
class Other { public: void PrintSomething(){} }; // 与 Animal 无继承关系
Dog* dog01 = new Dog;
Animal* animal02 = dynamic_cast&lt;Animal*&gt;(dog01); // 子类→父类,合法(其实用不着 dynamic_cast
// Dog* dog02 = dynamic_cast&lt;Dog*&gt;(animal01); // 父类→子类:运行期检查,失败返回 nullptr
// Animal* x = dynamic_cast&lt;Animal*&gt;(other); // 无继承关系:编译期报错
</code></pre>
<p><small>dynamic_cast 依赖 RTTI,只对多态类型(含虚函数)生效——这是与 static_cast
最本质的区别。出处:<a class="filelink" target="_blank"
href="../../../p01/ch06/dynamic_cast_demo.cpp">p01/ch06/dynamic_cast_demo.cpp</a></small></p>
<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>
@@ -77,34 +83,35 @@ Animal* animal02 = dynamic_cast&lt;Animal*&gt;(dog01); // 子类→父类,
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:模板与类型转换 <span class="tag tag-practice"></span></h2>
<h2>动手:Model/View + 事件 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 写一个通用容器模板 <span class="tag tag-practice"></span></h4>
<p>仿照 <a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_basics.cpp">class_template_basics.cpp</a>
写一个 <code>Pair&lt;T1, T2&gt;</code> 或简易 <code>Stack&lt;T&gt;</code>push/pop/top
栈空时的行为要明确定义)。</p>
<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 · dynamic_cast 安全向下转型 <span class="tag tag-practice"></span></h4>
<p>用 Day2 写的多态类层次(基类指针数组),对每个元素做
<code>dynamic_cast</code> 判断具体类型,转型失败(返回 nullptr)的分支要写出来,
不能假设转型一定成功。参照 <a class="filelink" target="_blank"
href="../../../p01/ch06/dynamic_cast_demo.cpp">dynamic_cast_demo.cpp</a></p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 选做:类模板的派生关系 <span class="tag tag-practice"></span></h4>
<p>阅读 <a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_derives_class.cpp">class_template_derives_class.cpp</a>
<a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_derives_template.cpp">class_template_derives_template.cpp</a>
写一句话总结「普通类继承类模板」与「类模板继承类模板」在写法上的区别。</p>
<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>
@@ -112,119 +119,82 @@ Animal* animal02 = dynamic_cast&lt;Animal*&gt;(dog01); // 子类→父类,
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min(三块内容合并速览)</p>
<h2>异常 + IO 流 + STL <span class="tag tag-lecture"></span></h2>
<ul>
<li>异常:栈解旋、自定义异常类</li>
<li>文件 IO:文本 vs 二进制</li>
<li>STL:容器 + 迭代器 + 算法 + 函数对象</li>
</ul>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>文件 IO / JSON 解析 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>栈解旋(stack unwinding <span class="tag tag-lecture"></span></h3>
<h3>文件系统:QFile 读文本 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Person{
public:
Person(string name){ mName = name; cout &lt;&lt; mName &lt;&lt; "对象被创建!\n"; }
~Person(){ cout &lt;&lt; mName &lt;&lt; "对象被析构!\n"; }
string mName;
};
void TestFunction(){
Person p1("aaa"), p2("bbb"), p3("ccc");
throw 10; // 抛出异常时,p3/p2/p1 按逆序自动析构——这就是「栈解旋」
}
int main(){ try{ TestFunction(); } catch(...){ cout&lt;&lt;"异常被捕获!\n"; } }
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="../../../p01/ch07/stack_unwinding.cpp">p01/ch07/stack_unwinding.cpp</a></small></p>
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>自定义异常类:继承 std::exception <span class="tag tag-lecture"></span></h3>
<h3>JSON 解析与写入(教学补充) <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class MyOutOfRange : public exception{
public:
MyOutOfRange(const string errorInfo){ m_Error = errorInfo; }
virtual const char* what() const noexcept { return m_Error.c_str(); }
string m_Error;
};
// 多个 catch 分支各自匹配不同的异常类型(都直接继承 exception,彼此是平级、不是父子)
// Person(151) 这里实际只会抛 MyOutOfRange,前两个 catch 分支不会命中——
// 写多个 catch 分支是为了同一段代码能同时应对「标准库异常」和「自定义异常」两种来源
try{ Person p(151); }
catch (out_of_range& e){ cout &lt;&lt; e.what(); } // 若改成 throw out_of_range(...) 会走这里
catch (MyOutOfRange e){ cout &lt;&lt; e.what(); } // 本例实际命中的分支
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>强化点:catch 分支的匹配顺序只在「类型存在继承关系」时才重要(子类 catch 必须写在
父类前面,否则父类分支会先吞掉子类异常);这里三个类型互不继承,顺序其实不影响本例结果。
出处:<a class="filelink" target="_blank"
href="../../../p01/ch07/custom_exception_class.cpp">p01/ch07/custom_exception_class.cpp</a></small></p>
</section>
<section>
<h3>二进制文件读写 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
ofstream osm("person.txt", ios::out | ios::binary);
osm.write((const char*)&p1, sizeof(Person)); // 直接写内存布局,不做任何格式转换
osm.close();
ifstream ism("person.txt", ios::in | ios::binary);
Person p3;
ism.read((char*)&p3, sizeof(Person)); // 按同样的 sizeof 读回——结构必须一致
</code></pre>
<p><small>二进制 IO 要求读写两端的结构体内存布局完全一致(含 padding),跨平台/跨编译器
不保证兼容。出处:<a class="filelink" target="_blank"
href="../../../p01/ch08/binary_file_io.cpp">p01/ch08/binary_file_io.cpp</a></small></p>
</section>
<section>
<h3>STL:容器 + 迭代器 + 算法 + 函数对象 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
// 谓词(函数对象)驱动算法
class GreaterThenFive{ public: bool operator()(int num){ return num &gt; 5; } };
vector&lt;int&gt;::iterator it = find_if(v.begin(), v.end(), GreaterThenFive());
class MyCompare{ public: bool operator()(int a,int b){ return a &gt; b; } };
sort(v.begin(), v.end(), MyCompare()); // 用函数对象改变算法策略——从小到大 → 从大到小
</code></pre>
<p><small>STL 的核心思想:容器负责存储,算法负责操作,迭代器是两者之间的胶水,
函数对象(谓词)负责定制算法行为。出处:
<a class="filelink" target="_blank" href="../../../p01/ch09/s03/vector_iterator.cpp">p01/ch09/s03/vector_iterator.cpp</a>
<a class="filelink" target="_blank" href="../../../p01/ch09/s04/predicate.cpp">p01/ch09/s04/predicate.cpp</a></small></p>
<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:练 ===================== -->
<!-- ===================== 模块 B:练(本地数据存储案例) ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min任选其二,第三个留参考</p>
<h2>动手:异常 / STL / 文件 IO <span class="tag tag-practice"></span></h2>
<p class="module-badge">模块 B · 练习 95min本地数据存储案例,练习任务,非预建方案</p>
<h2>动手:本地数据存储小工具 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 给类加异常处理 <span class="tag tag-practice"></span></h4>
<p>给 Day1/Day2 写的类加一个自定义异常类(继承 <code>std::exception</code>),
非法参数(如负数年龄)抛出异常,调用方 <code>catch</code> 后打印信息、程序不崩溃。
参照 <a class="filelink" target="_blank"
href="../../../p01/ch07/custom_exception_class.cpp">custom_exception_class.cpp</a></p>
<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 · vector + 算法解决实际问题 <span class="tag tag-practice"></span></h4>
<p><code>vector</code> 存一组「学生成绩」,用 <code>sort</code> +
自定义谓词按成绩排序,再用 <code>find_if</code> 找出所有不及格的学生并打印。
参照 <a class="filelink" target="_blank"
href="../../../p01/ch09/s04/predicate.cpp">predicate.cpp</a></p>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3 · 结构体数组的二进制读写 <span class="tag tag-practice"></span></h4>
<p>参照 <a class="filelink" target="_blank"
href="../../../p01/ch08/binary_file_io.cpp">binary_file_io.cpp</a>,把一个结构体数组
(如 5 个学生记录)写入二进制文件,再读出并逐条打印校验,确认与写入前一致。</p>
<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>
@@ -233,15 +203,14 @@ sort(v.begin(), v.end(), MyCompare()); // 用函数对象改变算法策略—
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>验收标准</h4>
<pre><code class="bash" data-trim>cmake --build build --target all_p01_ch05
cmake --build build --target all_p01_ch07
cmake --build build --target all_p01_ch09_s04</code></pre>
<p>全绿;至少 1 个模板类 + 1 个用 STL 容器/算法解决实际小问题的程序。</p>
<h4>现场演示</h4>
<p>现场演示本地数据存储小工具,验证"写入 → 重启 → 读回"完整闭环。</p>
</div>
<p class="footer-note"><small>日预告:Day 4 —— Qt 桌面全景(1 天快速建立骨架认知
+ 动手做一个可跑的小工具)</small></p>
<p><a class="filelink" href="index.html">返回目录</a></p>
<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>