幻灯片按 4 天版重排:Day1 全 C++、Day3 启用 ch05/ch06、Day5 综合案例

- Day1:开训 + 全天 C++(PRETEST 摸底环节内联;★所有权/★编译 vs 链接错误新卡)
- Day2:Qt 入门 + 信号槽 → 计算器(★栈控件双重释放/★lambda 第五参数/
  ★重复 connect/★QString 中文编码,均链到坑点复现示例)
- Day3:QMainWindow/QDialog(p03/ch05、ch06 首次排课,☆两张新卡)+ 布局/控件/QSS
- Day4:事件/绘图/QTableWidget + 文件系统/JSON(★工作目录/★文件编码)
- Day5:综合案例全天(student_manager 走查 + QThread 提升 + 流程规范)
- index.html 同步 4+1 结构;相对链接脚本校验 0 失效
This commit is contained in:
张宗平
2026-07-05 12:49:44 +08:00
parent 1053ebf41c
commit 0f81edcc74
6 changed files with 880 additions and 1132 deletions
+228 -278
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 1 · 开训 + C++ 回顾/工具链</title>
<title>Day 1 · 开训 + C++ 回顾与提升(全天)</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,277 +15,55 @@
<section>
<h1>Day 1</h1>
<h3>开训 + C++ 回顾/工具链</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 1 / 5 天 · 对齐官方方案书 Day1</p>
<h3>开训 + C++ 回顾与提升(全天 C++</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 1 / 4 个讲授日 · 对齐 2026-07 新版 10 天安排</p>
<ul>
<li>模块 A(3 课时):团队建设 / 敏捷 Scrum / C++ 在企业中的应用(讨论型,无编码)</li>
<li>模块 B(3 课时):C++ 环境搭建 / 高级程序回顾 / CMake / Vcpkg(压缩讲解)</li>
<li>上午(08:3011:00 讲 / 11:0012:00 练):开训 · C++ 技术版图 · 环境与 CMake · OOP 回顾</li>
<li>下午(13:3016:00 讲 / 16:0017:00 作业):继承与多态 · 内存管理 · STL 常用容器</li>
</ul>
<p><small>本教案假定学员已掌握 C++ 基础语法与 OOP 概念——方案书对今天下午的
官方定位是"回顾和提升",不是零基础教学。今天开始的 5 天覆盖方案书 C++方向
Day1–5 全部课堂内容,Day6–10 是纯项目实作。</small></p>
<p><small>本教案假定学员已掌握 C++ 基础语法与 OOP 概念——今天定位是"回顾和提升",
不是零基础教学。Day1–4 讲授,Day5 综合案例,Day6–10 项目开发与答辩。
大纲:<a class="filelink" target="_blank" href="../OUTLINE_4DAY.md">docs/teaching/OUTLINE_4DAY.md</a></small></p>
<div class="verify-box" style="font-size:0.5em;">
<h4>模块 B 开始前:10 分钟摸底测验</h4>
<p>这条假设不是拍脑袋定的——建议模块 B 讲授前先用
<a class="filelink" target="_blank" href="../PRETEST.md">docs/teaching/PRETEST.md</a>
(10 题、10 分钟)实测一次,按结果决定原节奏推进还是回退到语法讲解。</p>
<h4>OOP 回顾开讲前:10 分钟摸底测验</h4>
<p>受众假设要实测——用 <a class="filelink" target="_blank" href="../PRETEST.md">docs/teaching/PRETEST.md</a>
(10 题、10 分钟、A/B 平行卷)测一次,按结果决定回顾环节推进速度(分层预案见该文件)。</p>
</div>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<!-- ===================== 上午:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>团队建设 / 敏捷 Scrum / C++ 企业应用 <span class="tag tag-lecture"></span></h2>
<p class="module-badge">上午 · 讲授 08:3011:00(讲 2530min + 随堂练 1015min 循环)</p>
<h2>开训 / C++ 技术版图 / 环境与 CMake / OOP 回顾 <span class="tag tag-lecture"></span></h2>
<ul>
<li>团队建设与文化、实训安排考评要求</li>
<li>项目开发过程管理全景(JIRA / Git / OASIS / Wiki</li>
<li>敏捷 Scrum 项目开发管理</li>
<li>C++ 在企业中的应用</li>
<li>开训典礼:团队文化 · 实训安排 · 考评要求(组织方主持)</li>
<li>C++ 常用技术和框架(讲座</li>
<li>开发环境搭建 + Makefile/CMake 项目构建</li>
<li>PRETEST 摸底(10min)→ 面向对象基础回顾(类/对象/封装)</li>
</ul>
</section>
<section>
<h3>实习实训服务流程 <span class="tag tag-lecture"></span></h3>
<img src="../../images/training_service_flow.png" style="max-height:520px;">
<p><small>出处:<a class="filelink" target="_blank"
href="../../TRAINING_PLAN_2026.md">docs/TRAINING_PLAN_2026.md</a> 「六、实习实训服务流程」</small></p>
</section>
<section>
<h3>服务流程:核心概念 <span class="tag tag-lecture"></span></h3>
<h3>C++ 常用技术和框架(讲座) <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<h4>核心概念——C++ 的四大应用版图</h4>
<ul>
<li>需求分析→需求评审→项目研发/代码实现→阶段评审→项目测试/交付——每个环节都有明确的
"评审"节点把关,不是写完代码就结束,这就是接下来 Day6–10 项目实作要走的完整流程。</li>
<li><b>桌面客户端</b>:Qt(本次实训主线)、跨平台 GUI;</li>
<li><b>服务端/基础设施</b>:高性能网络服务、数据库引擎、游戏服务器;</li>
<li><b>嵌入式/车载</b>:资源受限环境里 C++ 仍是第一梯队选择;</li>
<li><b>游戏/图形</b>UE 引擎、渲染管线。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见误区</h4>
<ul>
<li>把"需求分析"当成走过场:跳过评审直接开始编码,后期返工成本远高于前期多花时间对齐需求。</li>
</ul>
</div>
</section>
<section>
<h3>质量保证:四平台一标准 <span class="tag tag-lecture"></span></h3>
<img src="../../images/quality_four_platforms.png" style="max-height:520px;">
<p><small>出处:<a class="filelink" target="_blank"
href="../../TRAINING_PLAN_2026.md">docs/TRAINING_PLAN_2026.md</a> 「七、质量保证」</small></p>
</section>
<section>
<h3>四平台一标准:核心概念 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<h4>而且它还在进化</h4>
<ul>
<li>JIRA(任务跟踪)+ Git(版本管理)+ Wiki 知识库 + OASIS 绿洲实训管理平台,四个工具
分别对应"谁在做什么/代码怎么变化/知识怎么沉淀/进度怎么被看到"四个不同维度,缺一个都会
让协作出现盲区。</li>
<li>C++11 起三年一版:11「几乎新语言」→ 17 实用大件 → 20 四大件(Concepts/Ranges/协程/模块)。
今天下午末尾有专门一页巡礼;深入自学:
<a class="filelink" target="_blank" href="../MODERN_CPP.md">MODERN_CPP.md</a> ·
<a class="filelink" target="_blank" href="../MODERN_CPP_20PLUS.md">MODERN_CPP_20PLUS.md</a></li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见误区</h4>
<ul>
<li>只用 Git 不用 Jira:代码有了版本历史,但"谁负责什么、进度到哪"没有可见记录,
团队协作时容易重复劳动或漏项。</li>
</ul>
</div>
</section>
</section>
<!-- ===================== 模块 A:讨论(无编码) ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讨论 95min(分组,非编码)</p>
<h2>动手:分组讨论 <span class="tag tag-discuss"></span></h2>
<p><small>今天上午没有编码练习——先把协作流程和项目方向定下来,Day6–10
的项目实作会真的用到今天讨论的结论。</small></p>
</section>
<section>
<div class="task-card discuss-card">
<h4>任务卡 1 · 协作分工 <span class="tag tag-discuss"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>结合服务流程图,说清楚本组打算怎么分工使用 Jira(任务跟踪)+ Git(代码版本管理)</dd>
<dt>验收标准</dt>
<dd>每组提交一句话分工说明(谁负责建 Jira 看板 / 谁负责 Git 仓库初始化),能在图上指出对应环节</dd>
<dt>基础知识 · 参考</dt>
<dd>上一节"实习实训服务流程"图 ·
<a class="filelink" target="_blank" href="../../TRAINING_PLAN_2026.md">TRAINING_PLAN_2026.md 「七、质量保证 (一)(二)」</a></dd>
</dl>
</div>
</section>
<section>
<div class="task-card discuss-card">
<h4>任务卡 2 · 选定项目方向 <span class="tag tag-discuss"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>选定实训期间打算做的项目方向(Linux C 方向,或 C++/Qt 方向的备选商业项目之一)</dd>
<dt>验收标准</dt>
<dd>每组报出项目名称 + 一句话说明选择理由</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../TRAINING_PLAN_2026.md">TRAINING_PLAN_2026.md 「八、备选商业项目介绍」</a></dd>
</dl>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min(巡礼式,压缩讲解)</p>
<h2>C++ 回顾 / CMake / Vcpkg <span class="tag tag-lecture"></span></h2>
<p><small>只挑「学过但容易讲不透」的点,不逐章展开——方案书官方定位是
"回顾和提升",深度让位于 Day2–5 的 Qt 内容。</small></p>
</section>
<section>
<h3>namespace 作用域 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>"作用域"不是"文件顺序"——很多人误以为 namespace 只是给名字加前缀,实际上它定义的是
一段可以被跨文件延续、跨块合并的作用域,同一个命名空间可以在多个文件里反复打开、追加内容。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>在头文件里写 <code>using namespace std;</code>:会把整个命名空间污染带给所有
include 这个头文件的地方,容易引发命名冲突,规范做法是只在 .cpp 里用,或干脆不用、
全程写限定名。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch03/namespace_syntax.cpp">p01/ch03/namespace_syntax.cpp</a>
对照 <a class="filelink" target="_blank" href="../../ERRATA.md">docs/ERRATA.md</a></small></p>
</section>
<section>
<h3>函数重载决议 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>编译器靠"名字改写"name mangling)把重载函数变成不同的符号;二义性往往不是代码写
错了,而是重载集合里存在两个"一样好"的候选,编译器无法替你选。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>隐式类型转换会扩大重载候选集——比如 <code>int</code>/<code>double</code> 都能转
<code>float</code> 参数,传一个 <code>long</code> 进去可能触发意料之外的重载版本。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch03/function_overload_mechanism.cpp">p01/ch03/function_overload_mechanism.cpp</a></small></p>
</section>
<section>
<h3>引用的本质 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>把引用理解成"自动解引用的 const 指针"——这个实现视角能解释引用为什么必须初始化、
为什么不能重新绑定到另一个对象。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>返回局部变量的引用:函数结束局部变量已销毁,返回的引用变成悬空引用,后续访问是
未定义行为——和返回局部变量指针是同一类坑。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch03/reference_essence.cpp">p01/ch03/reference_essence.cpp</a></small></p>
</section>
<section>
<h3>虚析构与内存泄漏 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
Base* p = new Derived();
delete p; // 如果 ~Base() 不是 virtual~Derived() 不会被调用——经典泄漏
</code></pre>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>通过基类指针 <code>delete</code> 派生类对象时,只有基类析构函数是 <code>virtual</code>
才会走虚函数表找到派生类的析构函数——这是"多态"在析构这个场景下的具体体现。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>只要一个类可能被继承、且可能通过基类指针删除派生对象,就应该把析构函数声明为
<code>virtual</code>——即使当前基类本身没什么要清理的,这是个防御性约定。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s08/virtual_dtor_purpose.cpp">p01/ch04/s08/virtual_dtor_purpose.cpp</a></small></p>
</section>
<section>
<h3>模板与 STL 速览 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>只点名,不展开:泛型编程(函数模板/类模板)让同一份代码适配不同类型,容器/迭代器
是 Day6–10 项目实作里会大量用到的基础设施——迭代器是"容器和算法之间的统一接口",这也是
STL 能让算法脱离具体容器类型复用的关键。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>容器扩容/删除元素后,之前保存的迭代器可能失效(如 <code>vector</code> 扩容),
继续用失效的迭代器是未定义行为——遍历中增删元素前要先弄清对应容器的迭代器失效规则。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_basics.cpp">p01/ch05/class_template_basics.cpp</a>
<a class="filelink" target="_blank" href="../../../p01/ch09/s03/vector_iterator.cpp">p01/ch09/s03/vector_iterator.cpp</a></small></p>
</section>
<section>
<h3>C++11/14/17/20+:一门仍在进化的语言 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念——每 3 年一次实打实的能力升级,不是小修小补</h4>
<ul>
<li><b>C++11</b>"现代 C++ 元年"):<code>auto</code> 类型推导、智能指针
<code>unique_ptr</code>/<code>shared_ptr</code>RAII 终结手动 <code>new</code>/<code>delete</code>)、
右值引用 <code>&amp;&amp;</code> + 移动语义(消除不必要的拷贝)、lambda 表达式、
语言级并发 <code>std::thread</code>/<code>mutex</code>/<code>atomic</code>
<code>nullptr</code><code>constexpr</code></li>
<li><b>C++14</b>:泛型 lambda(参数写 <code>auto</code>)、函数返回类型自动推导、
<code>std::make_unique</code> 补齐。</li>
<li><b>C++17</b>:结构化绑定 <code>auto [a,b] = pair</code><code>if</code>/<code>switch</code>
带初始化语句、<code>std::optional</code>/<code>variant</code>/<code>string_view</code>
标准库终于有跨平台的 <code>std::filesystem</code></li>
<li><b>C++20</b>(号称"自 C++11 以来最大的一次变革"):concepts 概念(模板约束,报错从
几百行天书变成一句话)、ranges 范围库(<code>view | filter | transform</code> 管道式组合)、
coroutines 协程(<code>co_await</code> 把异步代码写成同步的样子)、modules 模块(挑战头文件
体系)、<code>&lt;=&gt;</code> 三路比较一行换 6 个运算符。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>新标准 ≠ 现在能用:生产环境的编译器/CI 常年落后最新标准 1–2 个版本,用新特性前先确认
目标工具链 <code>-std=</code> 支持到哪。本仓库出于教学稳定性选 C++17 作为基线(顶层
<code>CMakeLists.txt</code>),可用 <code>tools/std_probe.py</code> 自己验证某特性最早从哪个
标准开始能编译(宿主 gcc 13.3 原生支持到 C++23)。</li>
</ul>
</div>
<div class="verify-box">
<h4>缺口说明</h4>
<p>本节为纯概念巡礼,无对应仓库示例——具体新特性的动手练习超出本次"C++回顾"压缩讲解范畴,
感兴趣可用 <code>tools/std_probe.py</code> + <code>tools/demo_versions/</code> 自行探索
(参见 <a class="filelink" target="_blank" href="../../VERSION_NOTES.md">docs/VERSION_NOTES.md</a>)。</p>
</div>
</section>
<section>
@@ -296,51 +74,72 @@ cmake --build build # 144 个目标全绿</code></pre>
<h4>核心概念</h4>
<ul>
<li>CMake 是"生成构建系统的工具"而不是构建系统本身:<code>cmake -S -B</code>
<code>CMakeLists.txt</code> 生成 Ninja/Makefile 工程,<code>cmake --build</code> 才是
真正编译。<code>option(BUILD_QT_PART)</code> 控制是否编译 Qt 部分,每个示例一个
<code>add_executable</code> 目标,这就是"一文件一目标"约定的由来。</li>
<code>CMakeLists.txt</code> 生成 Ninja/<b>Makefile</b> 工程(Makefile 是它的生成目标之一——
这就是两者的关系),<code>cmake --build</code> 才是真正编译。
<code>option(BUILD_QT_PART)</code> 控制是否编译 Qt 部分,每个示例一个
<code>add_executable</code> 目标。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>改了 <code>CMakeLists.txt</code> 却没重新 <code>cmake</code> 配置(只 <code>build</code>):
大多数改动 CMake 会自动侦测重新配置,但换生成器/清缓存等情况需要重新走
<code>-S -B</code> 一次。</li>
<li>改了 <code>CMakeLists.txt</code> 却没重新配置:大多数改动 CMake 会自动侦测,
但换生成器/清缓存需要重新走一次 <code>-S -B</code></li>
</ul>
</div>
<p><small>出处:仓库根 <a class="filelink" target="_blank" href="../../../CMakeLists.txt">CMakeLists.txt</a></small></p>
<p><small>出处:仓库根 <a class="filelink" target="_blank" href="../../../CMakeLists.txt">CMakeLists.txt</a>
· 包管理器一分钟:vcpkg 用 toolchain 文件让 <code>find_package</code> 自动找到三方库;
本仓库选"隔离 Qt 手动装",两种思路都要认识。</small></p>
</section>
<section>
<h3>Vcpkg 包管理器(概念,无演示) <span class="tag tag-lecture"></span></h3>
<h3>★ 编译错误 vs 链接错误:排查方向完全不同 <span class="tag tag-lecture"></span></h3>
<pre><code class="text" data-trim>main.cpp:12:5: error: 'foo' was not declared ← 编译期:带文件行号
main.o: undefined reference to `Widget::bar()' ← 链接期:没有行号</code></pre>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>C++ 生态的包管理器,解决"第三方库怎么装、怎么被 CMake 找到"的问题——一次
<code>vcpkg install</code>,配合 CMake 的 toolchain 文件,<code>find_package</code>
就能自动找到头文件和库</li>
<li><code>error:</code> 带行号 = 编译期问题(语法/类型/没声明)——回代码里查那一行;
<code>undefined reference</code> 无行号 = 链接期问题(有声明没定义/缺库/缺目标文件)——
查"定义在哪、链没链进来",回头检查语法是南辕北辙</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>不是所有第三方库都必须用包管理器装——本仓库用的是"隔离 Qt 手动装"
<code>tools/install_qt_host.sh</code>)而非 vcpkg,两种思路都要认识:包管理器省心但
引入额外工具链,手动隔离装则精确控制版本/路径,各有取舍。</li>
<li>中初级学员见到 <code>undefined reference</code> 的第一反应是检查拼写和语法——方向就错了。
明天 Qt 的"忘写 <code>Q_OBJECT</code>"报的正是链接错误,且报错完全不提宏,记住今天这页。</li>
</ul>
</div>
<div class="verify-box">
<h4>缺口说明</h4>
<p>本仓库暂无 vcpkg 可运行示例——安装真实 vcpkg 工具链超出最小示例范畴,此处仅作概念介绍。</p>
</section>
<section>
<h3>OOP 回顾:类 / 对象 / 封装 + namespace <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>封装 = 数据和操作绑定 + 访问控制(public/private 是给"人"划的边界,不是安全机制);
构造/析构对应对象生命周期的两端——这个心智模型下午的"所有权"还要用。</li>
<li>namespace 的"作用域"不是"文件顺序":可跨文件延续、跨块合并。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>头文件里 <code>using namespace std;</code> 会把命名空间污染带给所有 include 它的
地方——只在 .cpp 用,或全程写限定名。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p01/ch04/">p01/ch04/</a>(类与对象各节)、
<a class="filelink" target="_blank" href="../../../p01/ch03/namespace_syntax.cpp">p01/ch03/namespace_syntax.cpp</a>
对照 <a class="filelink" target="_blank" href="../../ERRATA.md">docs/ERRATA.md</a></small></p>
</section>
</section>
<!-- ===================== 模块 B:练 ===================== -->
<!-- ===================== 上午:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min</p>
<p class="module-badge">上午 · 课堂练习 11:00–12:00(工程师指导、答疑)</p>
<h2>动手:走一遍真实构建流程 <span class="tag tag-practice"></span></h2>
</section>
<section>
@@ -358,13 +157,165 @@ cmake --build build # 144 个目标全绿</code></pre>
</dl>
</div>
</section>
</section>
<!-- ===================== 下午:讲 ===================== -->
<section>
<section>
<p class="module-badge">下午 · 讲授 13:3016:00</p>
<h2>继承与多态 / 内存管理 / STL <span class="tag tag-lecture"></span></h2>
<ul>
<li>C++ 核心特性:继承与多态、虚函数(含虚析构)</li>
<li>内存管理:指针、引用与动态分配(含★所有权模型)</li>
<li>STL 常用容器简介</li>
<li>收尾:现代 C++ 巡礼(兴趣钩子)</li>
</ul>
</section>
<section>
<h3>函数重载决议 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>编译器靠"名字改写"name mangling)把重载函数变成不同符号;二义性往往不是代码写错,
而是重载集合里有两个"一样好"的候选,编译器无法替你选。重载(编译期,同类内) vs
重写(运行期,继承体系) 是两码事。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>隐式类型转换会扩大重载候选集——传一个 <code>long</code> 进去可能触发意料之外的重载版本。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch03/function_overload_mechanism.cpp">p01/ch03/function_overload_mechanism.cpp</a></small></p>
</section>
<section>
<h3>虚函数与虚析构 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
Base* p = new Derived();
delete p; // 如果 ~Base() 不是 virtual~Derived() 不会被调用——经典泄漏
</code></pre>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>通过基类指针 <code>delete</code> 派生类对象时,只有基类析构是 <code>virtual</code>
才会走虚函数表找到派生类析构——"多态"在析构场景下的具体体现。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>只要类可能被继承 + 可能经基类指针删除,析构就该 <code>virtual</code>——防御性约定。
(明天会看到 Qt 的 QObject 体系全线虚析构。)</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch04/s08/virtual_dtor_purpose.cpp">p01/ch04/s08/virtual_dtor_purpose.cpp</a></small></p>
</section>
<section>
<h3>引用的本质 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>把引用理解成"自动解引用的 const 指针"——这个实现视角能解释为什么必须初始化、
为什么不能重新绑定。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>返回局部变量的引用:函数结束局部变量已销毁,引用悬空,访问是未定义行为——
和返回局部变量指针同一类坑。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch03/reference_essence.cpp">p01/ch03/reference_essence.cpp</a></small></p>
</section>
<section>
<h3>★ 所有权:谁负责 delete <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>内存管理的核心问题不是"会不会 new",而是<b>"这块资源归谁管、由谁释放"</b>
现代答案是 RAII/智能指针:<code>unique_ptr</code> 独占所有权、<code>shared_ptr</code>
共享所有权(详见 <a class="filelink" target="_blank" href="../MODERN_CPP.md">MODERN_CPP.md</a> §2)。
一个对象只能有一个所有者。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>双重释放、忘记释放、释放后使用——三兄弟全部源于所有权不明。</li>
<li><b>明天的伏笔</b>Qt 用"父子树"回答同一个问题(parent 析构自动 delete 子对象)。
两套所有权体系<b>不能混用</b>——交给 Qt 树管的对象,不要再用智能指针/栈/手动 delete 管。</li>
</ul>
</div>
</section>
<section>
<h3>模板与 STL 常用容器 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>泛型编程让同一份代码适配不同类型;<code>vector</code>/<code>map</code>/<code>string</code>
是项目实作三件套;迭代器是"容器和算法之间的统一接口"——STL 算法能脱离具体容器复用的关键。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>容器扩容/删除元素后,旧迭代器可能失效(如 <code>vector</code> 扩容搬家),继续用是
未定义行为——遍历中增删元素前先弄清该容器的失效规则。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p01/ch05/class_template_basics.cpp">p01/ch05/class_template_basics.cpp</a>
<a class="filelink" target="_blank" href="../../../p01/ch09/s03/vector_iterator.cpp">p01/ch09/s03/vector_iterator.cpp</a></small></p>
</section>
<section>
<h3>钩子 · C++11/14/17/20+:一门仍在进化的语言 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念——每 3 年一次实打实的能力升级</h4>
<ul>
<li><b>C++11</b>:auto · 智能指针 · 移动语义 · lambda · 语言级并发 —— "现代 C++ 元年"</li>
<li><b>C++14</b>:泛型 lambda · 返回类型推导 · <code>make_unique</code></li>
<li><b>C++17</b>:结构化绑定 · if-init · optional/variant/string_view · filesystem(本仓库基线);</li>
<li><b>C++20</b>Concepts · Ranges · 协程 · 模块 —— "自 C++11 以来最大变革"。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>新标准 ≠ 现在能用:生产工具链常年落后 1–2 版。用 <code>tools/std_probe.py</code>
自己验证某特性从哪个标准开始能编译(宿主 gcc 13.3 支持到 C++23)。</li>
</ul>
</div>
<p><small>自学入口:<a class="filelink" target="_blank" href="../MODERN_CPP.md">MODERN_CPP.md</a>11/14/17
示例全部可在本仓库基线下编译) ·
<a class="filelink" target="_blank" href="../MODERN_CPP_20PLUS.md">MODERN_CPP_20PLUS.md</a>20/23/26 前瞻,
含本机 GCC 实测矩阵)</small></p>
</section>
</section>
<!-- ===================== 下午:作业 ===================== -->
<section>
<section>
<p class="module-badge">下午 · 课堂作业 16:00–17:00(工程师答疑)</p>
<h2>作业 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 修一个真实的坑 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd><a class="filelink" target="_blank" href="../../ERRATA.md">docs/ERRATA.md</a>
挑 2 条记录,理解"wiki 原文错在哪、现在的写法为什么"</dd>
挑 2 条与今天知识点相关的记录(虚析构/引用/重载类),理解"wiki 原文错在哪、为什么"</dd>
<dt>验收标准</dt>
<dd>能用一句话向同组同学解释清楚,不能只念代码</dd>
<dt>基础知识 · 参考</dt>
@@ -377,13 +328,12 @@ cmake --build build # 144 个目标全绿</code></pre>
<h4>任务卡 3(选做)· 版本探针 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd><code>tools/std_probe.py</code> 验证一个标准版本差异点(如三目运算符可否作左值)</dd>
<dd><code>tools/std_probe.py</code> 验证一个标准版本差异点</dd>
<dt>验收标准</dt>
<dd>跑出至少一个"该标准报错、另一个标准不报错"的对比结果,对照
<dd>跑出至少一个"该标准报错、另一个标准不报错"的对比,对照
<a class="filelink" target="_blank" href="../../VERSION_NOTES.md">docs/VERSION_NOTES.md</a> §2</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../tools/std_probe.py">tools/std_probe.py</a> ·
<a class="filelink" target="_blank" href="../../VERSION_NOTES.md">docs/VERSION_NOTES.md</a></dd>
<dd><a class="filelink" target="_blank" href="../../../tools/std_probe.py">tools/std_probe.py</a></dd>
</dl>
</div>
</section>
@@ -395,10 +345,10 @@ cmake --build build # 144 个目标全绿</code></pre>
<div class="verify-box">
<h4>验收</h4>
<pre><code class="bash" data-trim>cmake --build build --target all # Part1144 个目标全绿</code></pre>
<p>组提交一句话分工说明 + 至少 1 条 ERRATA 坑点讲解</p>
<p>人完成构建全绿 + 2 条 ERRATA 坑点讲解;PRETEST 结果已归档(决定后续调速)</p>
</div>
<p class="footer-note"><small>明天 Day2 进入 Qt项目结构 / 信号槽 / 控件 / 布局与 QSS ——
大纲:<a class="filelink" href="../OUTLINE.md">docs/teaching/OUTLINE.md</a></small></p>
<p class="footer-note"><small>明天 Day2 进入 Qt概述 / 创建项目 / 第一个程序 / 信号槽 → 计算器 ——
大纲:<a class="filelink" href="../OUTLINE_4DAY.md">docs/teaching/OUTLINE_4DAY.md</a></small></p>
<p><a class="filelink" href="index.html">返回目录</a> · <a class="filelink" href="day2.html">下一天 →</a></p>
</section>
+199 -285
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 2 · Qt 开发环境/信号槽/基础控件/布局与 QSS</title>
<title>Day 2 · Qt 入门 / 信号槽 / 计算器案例</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 2</h1>
<h3>Qt 开发环境 / 信号槽 / 基础控件 / 布局与 QSS</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 2 / 5 天 · 讲:练 ≈ 1:2.7</p>
<h3>Qt 概述 / 创建项目 / 第一个程序 / 信号槽 → 计算器</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 2 / 4 个讲授日</p>
<ul>
<li>模块 A(3 课时):项目结构 / 第一个程序 / 信号槽</li>
<li>模块 B(3 课时):常用控件 / 布局管理 / QSS 界面美化 → 计算器案例</li>
<li>上午(08:3011:00 讲 / 11:0012:00 练):Qt 概述 · 创建 Qt 项目 · 第一个 Qt 小程序</li>
<li>下午(13:3016:00 讲 / 16:0017:00 作业):信号槽机制 → 项目案例:简易计算器</li>
</ul>
<div class="verify-box" style="font-size:0.5em;">
<h4>开始前准备(一次性)</h4>
@@ -30,355 +30,270 @@ cmake --build build</code></pre>
</div>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<!-- ===================== 上午:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>项目结构 / 信号槽 <span class="tag tag-lecture"></span></h2>
<ul>
<li>最小 Qt 程序结构</li>
<li>控件创建与显示</li>
<li>内置 / 自定义 / lambda 信号槽</li>
</ul>
<p class="module-badge">上午 · 讲授 08:3011:00</p>
<h2>Qt 概述 / 创建项目 / 第一个程序 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>最小 Qt 程序:核心概念 <span class="tag tag-lecture"></span></h3>
<h3>Qt 概述 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><code>QApplication</code> 是整个进程唯一的"事件循环拥有者"——<code>app.exec()</code>
阻塞在这里进入事件循环,不断从操作系统取事件(鼠标/键盘/定时器/重绘请求)分发出去,
程序的"活着"就是这个循环在转,不是运行完就退出的普通程序。</li>
<li><code>Q_OBJECT</code> 宏触发 mocmeta-object compiler)在编译前生成额外代码——
信号槽、属性系统、<code>qobject_cast</code> 等运行时反射能力都靠这份生成代码实现,
这是 Qt 自己的预处理步骤,不是普通 C++ 语法。</li>
<li>Qt = 跨平台 C++ 应用框架:一份代码,Windows/Linux/macOS 原生编译;模块化组织——
本课程用 <code>Core</code>(对象/容器/IO)、<code>Gui</code><code>Widgets</code>(桌面控件)。</li>
<li>版本纪律:本实训统一用<b>隔离安装的 Qt 5.14.2</b><code>.qt514/</code>),不碰系统
Qt——顶层 CMakeLists 有版本断言,连错版本直接 <code>FATAL_ERROR</code>。这是工程上
"锁依赖版本"的真实示范。</li>
</ul>
</div>
<p><small>出处:仓库根 <a class="filelink" target="_blank" href="../../../CMakeLists.txt">CMakeLists.txt</a>(版本断言一节)</small></p>
</section>
<section>
<h3>创建 Qt 项目(CMake 方式) <span class="tag tag-lecture"></span></h3>
<pre><code class="cmake" data-trim>find_package(Qt5 5.14 REQUIRED COMPONENTS Core Gui Widgets)
add_executable(minimal_qt_app minimal_qt_app.cpp)
set_target_properties(minimal_qt_app PROPERTIES AUTOMOC ON)
target_link_libraries(minimal_qt_app PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)</code></pre>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>昨天的 <code>find_package</code> 概念今天落地:CMake 按 <code>CMAKE_PREFIX_PATH</code>
找到 Qt<code>AUTOMOC</code> 自动跑 moc(元对象编译器)——它为 <code>Q_OBJECT</code>
类生成信号槽所需的代码,下一页就见到。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/CMakeLists.txt">p03/CMakeLists.txt</a>
<a class="filelink" target="_blank" href="../../../p03/ch02/CMakeLists.txt">p03/ch02/CMakeLists.txt</a></small></p>
</section>
<section>
<h3>第一个 Qt 小程序 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><code>QApplication</code> 是唯一的事件循环拥有者,<code>app.exec()</code> 阻塞进入
事件循环分发系统事件;<code>Q_OBJECT</code> 宏触发 moc 生成信号槽/属性系统所需的元对象代码。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>一个进程只能有一个 <code>QApplication</code> 实例,创建第二个直接崩溃/未定义行为。</li>
<li>忘记调<code>exec()</code>窗口一闪而过就退出,因为根本没进入事件循环。</li>
<li>类里用了信号槽却忘记写 <code>Q_OBJECT</code>:编译能过,链接阶段报
<code>undefined reference</code>——报错信息完全不提示"缺宏",是新手最常踩的坑。</li>
<li>一个进程只能有一个 <code>QApplication</code>;忘记 <code>exec()</code> 窗口一闪而过;</li>
<li>了信号槽却忘 <code>Q_OBJECT</code><code>undefined reference</code>——
<b>链接错误</b>(昨天那页!),报错完全不提"缺宏",知道这一条能省一小时。</li>
</ul>
</div>
<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>最小 Qt 程序:QApplication + exec() <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Widget : public QWidget {
Q_OBJECT // 用到信号槽/属性系统的类都要有这个宏(触发 moc 代码生成)
public:
Widget(QWidget* parent = nullptr) : QWidget(parent) {}
};
int main(int argc, char* argv[]) {
QApplication app(argc, argv); // 整个程序唯一的 QApplication
Widget w;
w.show();
return app.exec(); // 进入事件循环,程序在这里“活着”
}
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch02/minimal_qt_app.cpp">p03/ch02/minimal_qt_app.cpp</a>(注意仓库版本额外注入了
<code>QTimer::singleShot</code> 200ms 自动退出,便于离屏自动化验证,真实项目不需要这一行)</small></p>
</section>
<section>
<h3>控件创建:对象树与父子所有权 <span class="tag tag-lecture"></span></h3>
<h3>控件创建与对象树 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>QObject 有一套"对象树"父子所有权模型:给控件传入 <code>parent</code> 后,
parent 被销毁时会自动 <code>delete</code> 所有子对象——这就是为什么 Qt 代码里到处是
<code>new</code> 却几乎不见 <code>delete</code></li>
<li>两种写法(先构造后设属性 / 构造时一次性传参)本质相同,父对象的作用一样。</li>
<li>QObject 对象树 = Qt 的<b>所有权</b>答案(接昨天"谁负责 delete"):创建控件把 parent
传进去,parent 析构自动 delete 所有子对象——整棵树只需要管好根。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>忘记传 <code>parent</code>:控件永远不会自动释放(真实内存泄漏),也不会自动出现在
窗口里(没有父容器就没有布局位置)</li>
<li><code>move()</code> 手动摆放坐标的控件,一旦外层套上布局管理器,手动坐标会被
布局在下次刷新时覆盖——这是"混用绝对定位和布局"最常见的诡异 bug 来源。</li>
<li>忘记传 parent:对象没人管,真实内存泄漏;</li>
<li><code>move()</code> 手动定位的控件一旦套上布局会被布局接管覆盖</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.cpp</a></small></p>
</section>
<section>
<h3>控件创建:两种写法 <span class="tag tag-lecture"></span></h3>
<h3>★ 栈对象与父子所有权打架 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
// 第一种:先构造再设属性
QPushButton* btn = new QPushButton;
btn->setParent(this);
btn->setText(QStringLiteral("德玛西亚"));
btn->move(100, 100);
// 第二种:构造时指定文本与父亲
QPushButton* btn2 = new QPushButton(QStringLiteral("孙悟空"), this);
QPushButton btn("我建在栈上"); // 先声明 → 后析构
QWidget window; // 后声明 → 先析构
btn.setParent(&window);
// 离开作用域:window 先析构 → 父子树 delete &btn(栈地址!)→ 崩溃
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.cpp</a></small></p>
</section>
<section>
<h3>信号槽:观察者模式 + moc <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>信号槽本质是"观察者模式"的 Qt 实现:发送者不知道、也不关心谁在监听,彻底解耦发送方
和接收方——这和"直接调用某个函数"是完全不同的设计取向</li>
<li><code>connect()</code> 背后依赖 moc 生成的元对象信息做类型检查:新语法
<code>&Class::signal</code><strong>编译期</strong>检查,字符串旧语法
<code>SIGNAL()/SLOT()</code><strong>运行时</strong>检查,容易悄悄连接失败。</li>
<li>连接方式分 Direct(同线程直接调用)/ Queued(跨线程排队,下次事件循环处理)/
Auto(默认,同线程 Direct、跨线程自动 Queued)——<a class="filelink" target="_blank"
href="day4.html">Day4</a> 讲 QThread 时还会用到。</li>
<li>父子树会 delete 子对象 ⇒ <b>交给父子树管的对象必须建在堆上</b>。一个对象只能有
一个所有者:给了 parent,就不能再由栈/智能指针/手动 delete 管</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>旧的字符串语法 <code>SIGNAL(clicked())</code> 函数名/参数类型写错不会编译报错,
只会运行时"连接失败"且经常没有提示——能用新语法就不用旧语法,编译期就能发现问题</li>
<li>重载信号(如右侧例子的两个 <code>hungry()</code>)不加 <code>QOverload</code>
消歧义,编译器不知道该连哪个重载,直接报错。</li>
<li>声明顺序反过来时这段代码"恰好不崩"——不要赌析构顺序的运气,规则只有一条。
现场演示:<code>tools/run_qt.sh qt_pitfall_stack_widget --crash</code>(真崩给你看)</li>
</ul>
</div>
</section>
<section>
<h3>信号槽:内置 / 自定义 / 重载消歧义 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Teacher : public QObject {
Q_OBJECT
signals:
void hungry(); // 无参信号
void hungry(QString food); // 带参信号(重载)
public slots:
void classIsOver(){ emit hungry(); emit hungry(QString("汉堡")); }
};
// 连接重载信号时必须用 QOverload 显式指定版本
connect(t, QOverload&lt;&gt;::of(&Teacher::hungry), s, QOverload&lt;&gt;::of(&Student::treat));
connect(t, QOverload&lt;QString&gt;::of(&Teacher::hungry), s, QOverload&lt;QString&gt;::of(&Student::treat));
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch04/custom_signal_slot.cpp">p03/ch04/custom_signal_slot.cpp</a>
lambda 槽 + 值捕获/引用捕获对比见
<a class="filelink" target="_blank" href="../../../p03/ch04/lambda_signal_slot.cpp">lambda_signal_slot.cpp</a></small></p>
<p><small>复现示例:<a class="filelink" target="_blank"
href="../examples/pitfall_stack_widget/qt_pitfall_stack_widget.cpp">examples/pitfall_stack_widget/</a>
<a class="filelink" target="_blank" href="../examples/pitfall_stack_widget/README.md">README</a></small></p>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<!-- ===================== 上午:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:第一个 Qt 程序 <span class="tag tag-practice"></span></h2>
<p class="module-badge">上午 · 课堂练习 11:00–12:00(工程师指导、答疑)</p>
<h2>动手:跑通最小程序 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 跑通最小程序 <span class="tag tag-practice"></span></h4>
<h4>任务卡 1 · 跑通并改造最小程序 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>编译运行 <code>minimal_qt_app</code><code>button_creation</code></dd>
<dd>编译运行 <code>minimal_qt_app</code><code>button_creation</code>;把窗口标题和
按钮文字改成自己的学号+姓名(<b>中文</b>——如果乱码了,记住现象,下午有一页专讲)</dd>
<dt>验收标准</dt>
<dd><code>QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app</code>
<code>button_creation</code> 均退出码为 0,无崩溃</dd>
<dd><code>QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app</code> 退出码 0
有 X 环境下能看到自己改的中文标题正常显示</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch02/minimal_qt_app.cpp">p03/ch02/minimal_qt_app.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.cpp</a></dd>
</dl>
</div>
</section>
</section>
<!-- ===================== 下午:讲 ===================== -->
<section>
<section>
<div class="task-card">
<h4>任务卡 2 · 自定义信号槽改文本 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>给一个按钮连接自定义信号槽:点击后改变一个 <code>QLabel</code> 的文本内容</dd>
<dt>验收标准</dt>
<dd>编译通过;离屏或有 X 环境下点击/模拟点击后 <code>QLabel</code> 文本确实改变;
<code>connect</code> 语法而非直接函数调用</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch04/custom_signal_slot.cpp">custom_signal_slot.cpp</a>(主参考)·
<a class="filelink" target="_blank" href="../../../p03/ch04/lambda_signal_slot.cpp">lambda_signal_slot.cpp</a>lambda 槽写法对比)</dd>
</dl>
<p class="module-badge">下午 · 讲授 13:3016:00</p>
<h2>信号槽机制 → 计算器案例 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>信号槽:内置 / 自定义 / lambda <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>观察者模式的 Qt 实现:发送者只 <code>emit</code>,不知道谁在听——对象间彻底解耦;</li>
<li>新语法 <code>&amp;Class::signal</code> 编译期类型检查;旧字符串语法
<code>SIGNAL()/SLOT()</code> 只在运行时检查;连接方式 Direct/Queued/AutoDay5 跨线程时回收)。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>旧语法写错<b>不报编译错误</b>,只是运行时静默连接失败;</li>
<li>重载信号不加 <code>QOverload</code> 消歧义直接编译报错。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch04/builtin_signal_slot.cpp">p03/ch04/builtin_signal_slot.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch04/custom_signal_slot.cpp">custom_signal_slot.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch04/lambda_signal_slot.cpp">lambda_signal_slot.cpp</a></small></p>
</section>
<section>
<h3>★ lambda 槽的生命周期与第五参数 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
connect(sender, &Sender::sig, [obj]{ obj->f(); }); // ✗ obj 销毁后仍触发
connect(sender, &Sender::sig, obj, [obj]{ obj->f(); }); // ✓ 第五参数=context
</code></pre>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>第五参数(context object)决定连接跟谁的生命周期挂钩:传了 context,对象析构时
Qt 自动断开连接。规则:<b>lambda 捕获了某个 QObject,就把它作为第五参数传进去</b></li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>不传 context:接收方销毁后 lambda 仍被调用,捕获的指针就是悬空指针——UB 可能崩、
也可能"打印出看似正常的旧值",后者更危险(开发机正常、演示现场炸)。</li>
</ul>
</div>
<p><small>复现示例:<a class="filelink" target="_blank"
href="../examples/pitfall_lambda_capture/qt_pitfall_lambda_capture.cpp">examples/pitfall_lambda_capture/</a>
<a class="filelink" target="_blank" href="../examples/pitfall_lambda_capture/README.md">README</a>
<code>--dangle</code> 看真实悬空访问)</small></p>
</section>
<section>
<h3>★ 重复 connect:点一次按钮弹两次框 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><code>connect</code> 每调一次<b>新增一条</b>连接,Qt 不去重——同一对信号槽连两次,
槽就被调两次。修复:connect 收拢到构造函数只做一次(首选);或
<code>Qt::UniqueConnection</code>(只对成员函数指针有效,lambda 防不了)。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>connect 写在会被多次执行的路径上(每次打开对话框都跑一遍的"初始化"函数)——
现象是"点一次弹两次",第一反应查按钮,根源在连接次数。</li>
</ul>
</div>
<p><small>复现示例:<a class="filelink" target="_blank"
href="../examples/pitfall_double_connect/qt_pitfall_double_connect.cpp">examples/pitfall_double_connect/</a>
<a class="filelink" target="_blank" href="../examples/pitfall_double_connect/README.md">README</a></small></p>
</section>
<section>
<h3>★ QString 与中文编码 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><code>QString</code> 内部是 UTF-16,和 <code>std::string</code>(字节串)是两种东西,
互转必须指明编码:<code>toUtf8()</code>/<code>fromUtf8()</code>;中文字面量建议
<code>QStringLiteral("中文")</code> + 源码文件保存为 UTF-8。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>源码不是 UTF-8、或用 <code>QString(char*)</code> 隐式转换中文字面量 → 界面乱码。
上午改学号姓名乱码的同学:根源就在这页。Day4 读写文件时编码问题还会再来一次。</li>
</ul>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3(选做)· 重载信号消歧义 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>仿 <code>builtin_signal_slot.cpp</code>,给一个类定义两个同名不同参的信号,用
<code>QOverload</code> 消歧义连接</dd>
<dt>验收标准</dt>
<dd>两个重载信号都能各自触发对应的槽</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch04/builtin_signal_slot.cpp">p03/ch04/builtin_signal_slot.cpp</a></dd>
</dl>
<h3>案例 · 简易计算器(布局脚手架:先用后学) <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
auto* grid = new QGridLayout(window); // 布局原理明天下午讲透
grid->addWidget(btn7, 0, 0); grid->addWidget(btn8, 0, 1); // 今天先会用:
grid->addWidget(btn9, 0, 2); grid->addWidget(btnDiv, 0, 3); // 行、列坐标摆键盘
</code></pre>
<div class="concept-box">
<h4>组装思路</h4>
<ul>
<li>数字/运算符按钮(上午的 <code>button_creation</code>+ 点击信号槽(刚学的三种写法)
+ <code>QGridLayout</code> 脚手架排键盘 + <code>QLabel</code>/<code>QLineEdit</code> 显示结果。</li>
<li>项目 = 已学知识点的组装——这是 Day5 综合案例和 Day6–10 项目的预演。</li>
</ul>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<!-- ===================== 下午:作业 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>常用控件 / 布局管理 / QSS 美化 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>资源系统:.qrc 是什么 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><code>:/Image/boat.jpg</code> 这种路径不是文件系统路径,是 Qt 资源系统(QRC)——
图片等资源在编译期被打包进可执行文件本身,运行时不依赖外部文件是否存在,这对
"打包发布"很关键。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li><code>.qrc</code> 里资源路径/文件名和代码里 <code>:/...</code> 引用的不一致,会在
运行时静默失败(<code>QPixmap</code> 为空、图不显示),不会报编译错误。</li>
<li>忘记把 <code>.qrc</code> 接入 CMake 的 <code>qt5_add_resources</code>/AUTORCC
资源系统压根不生效。</li>
</ul>
</div>
</section>
<section>
<h3>常用控件:QLabel 显示图片 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
QPixmap pixmap;
pixmap.load(":/Image/boat.jpg"); // 从 Qt 资源系统(.qrc)加载
QLabel* label = new QLabel(this);
label->setPixmap(pixmap);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch08/label_pixmap.cpp">p03/ch08/label_pixmap.cpp</a>
HTML 富文本标签见 <a class="filelink" target="_blank" href="../../../p03/ch08/label_text_html.cpp">label_text_html.cpp</a></small></p>
</section>
<section>
<h3>布局管理:为什么不手算坐标 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>布局管理器(<code>QLayout</code>)接管子控件的位置和大小计算,随窗口缩放自动重新
排布——这是"不要自己算坐标"的根本原因,也是跨平台/跨分辨率 UI 能正确显示的基础。</li>
<li>布局对象本身也遵循 QObject 父子所有权:<code>new QHBoxLayout(this)</code>
把布局设为 <code>this</code> 的布局,同时隐式接管了 <code>addWidget</code> 进来的子控件。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>同一个控件不能同时属于两个布局,重复 <code>addWidget</code> 到不同布局会导致界面
异常或崩溃。</li>
<li>布局设置之后又用 <code>setGeometry</code>/<code>move</code> 手动改控件位置,效果
会被布局在下次刷新时覆盖(呼应上一节"控件创建"的坑)。</li>
</ul>
</div>
</section>
<section>
<h3>自定义控件与布局:QHBoxLayout <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
QSpinBox* spin = new QSpinBox(this);
QSlider* slider = new QSlider(Qt::Horizontal, this);
QHBoxLayout* layout = new QHBoxLayout(this);
layout->addWidget(spin);
layout->addWidget(slider);
// spin 变化 → slider 跟随,slider 变化 → spin 跟随
connect(spin, static_cast&lt;void (QSpinBox::*)(int)&gt;(&QSpinBox::valueChanged), slider, &QSlider::setValue);
connect(slider, &QSlider::valueChanged, spin, &QSpinBox::setValue);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch08/custom_widget.cpp">p03/ch08/custom_widget.cpp</a>——布局管理器负责排布,
不用手算控件坐标。</small></p>
</section>
<section>
<h3>QSS:类 CSS 的样式层叠 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>QSS 语法脱胎于 CSS:选择器(类型选择器 <code>QPushButton</code>、ID 选择器
<code>#okButton</code>、伪状态 <code>:hover</code>/<code>:pressed</code>+ 属性声明,
但作用对象是 QWidget 树而不是 DOM。</li>
<li>样式表遵循层叠规则:作用在父控件上的样式默认会被子控件继承,除非子控件有自己更
具体的规则覆盖。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>用 ID 选择器(<code>#okButton</code>)必须先 <code>setObjectName("okButton")</code>
忘记设置会导致规则完全不生效且没有任何报错。</li>
<li>在某个容器上 <code>setStyleSheet</code> 会级联影响它所有子控件(包括不想改的),
范围失控是 QSS 最容易踩的坑,通常要用更具体的选择器限定范围。</li>
</ul>
</div>
</section>
<section>
<h3>QSS 界面美化(教学补充) <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
okBtn->setObjectName("okButton");
setStyleSheet(
"QPushButton#okButton { background-color: #2f855a; border-radius: 6px; color: white; }"
"QPushButton#okButton:hover { background-color: #276749; }"
);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../examples/qss_styling/qt_qss_styling.cpp">docs/teaching/examples/qss_styling/qt_qss_styling.cpp</a>
<a class="filelink" target="_blank" href="../examples/qss_styling/README.md">配套 README</a>)——
仓库 <code>p03/</code> 的 wiki 抓取内容没有 QSS,这是本轮补写的教学补充示例,已离屏验证跑通。</small></p>
</section>
</section>
<!-- ===================== 模块 B:练(计算器案例) ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min(计算器案例,练习任务,非预建方案)</p>
<h2>动手:做一个计算器 <span class="tag tag-practice"></span></h2>
<p class="module-badge">下午 · 课堂作业 16:00–17:00(工程师答疑)</p>
<h2>作业:完成计算器 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 计算器雏形 <span class="tag tag-practice"></span></h4>
<h4>任务卡 2 · 简易计算器 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>组合 <code>button_creation.cpp</code>(数字/运算符按钮+ <code>custom_signal_slot.cpp</code>
(点击信号槽)+ <code>custom_widget.cpp</code>(布局排布按钮),做出一个能加减乘除的最小计算器</dd>
<dd>组合按钮 + 信号槽 + QGridLayout 脚手架,做出能加减乘除的最小计算器</dd>
<dt>验收标准</dt>
<dd>至少支持两个数 + 一种运算符的计算并<code>QLabel</code>/<code>QLineEdit</code>正确显示结果;
界面不重叠、按钮可点击</dd>
<dd>至少支持两个数 + 一种运算符的计算并正确显示结果;界面不重叠、按钮可点击
connect 全部用新语法且写在构造函数里(今天的坑卡现学现用)</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch03/button_creation.cpp">button_creation.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch04/custom_signal_slot.cpp">custom_signal_slot.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch08/custom_widget.cpp">custom_widget.cpp</a></dd>
</dl>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 用 QSS 美化计算器 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>给任务卡 1 的计算器套一份 QSS 样式(数字键/运算键/等号键区分颜色)</dd>
<dt>验收标准</dt>
<dd>至少 2 类按钮有不同的 <code>background-color</code>;至少 1 个按钮有
<code>:hover</code><code>:pressed</code> 效果</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../examples/qss_styling/qt_qss_styling.cpp">qt_qss_styling.cpp</a> ·
<a class="filelink" target="_blank" href="../examples/qss_styling/README.md">README</a></dd>
<dd><a class="filelink" target="_blank" href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch04/custom_signal_slot.cpp">p03/ch04/custom_signal_slot.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch08/custom_widget.cpp">p03/ch08/custom_widget.cpp</a>(布局参考)</dd>
</dl>
</div>
</section>
@@ -388,15 +303,14 @@ setStyleSheet(
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>现场演示</h4>
<pre><code class="bash" data-trim>QT_QPA_PLATFORM=offscreen tools/run_qt.sh &lt;target&gt;
# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口</code></pre>
<p>现场演示计算器,至少完成一次正确运算 + QSS 美化。</p>
<h4>验收</h4>
<p>现场演示计算器完成至少一次正确运算(<code>QT_QPA_PLATFORM=offscreen tools/run_qt.sh &lt;target&gt;</code>
有 X 环境直接运行);中文界面文字无乱码。</p>
</div>
<p class="footer-note"><small>明天 Day3:高级控件 Model/View / 事件机制 / 文件 IO 与 JSON ——
大纲:<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="day1.html">← 上一天</a> ·
<p class="footer-note"><small>钩子 · 信号槽的幕后:打开 <code>build/</code> 里真实的
<code>moc_*.cpp</code>,看看 moc 到底生成了什么代码。<br>
明天 Day3QMainWindow / QDialog / 布局管理器 / 常用控件——计算器要升级成"正经软件"。</small></p>
<p><a class="filelink" href="index.html">返回目录</a> · <a class="filelink" href="day1.html">← 上一天</a> ·
<a class="filelink" href="day3.html">下一天 →</a></p>
</section>
+121 -213
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 3 · 高级控件 Model/View / 事件机制 / 文件IO与JSON</title>
<title>Day 3 · QMainWindow / QDialog / 布局与控件 / QSS</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,303 +15,211 @@
<section>
<h1>Day 3</h1>
<h3>高级控件 Model/View / 事件机制 / 文件 IO 与 JSON</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 3 / 5 天 · 讲:练 ≈ 1:2.7</p>
<h3>QMainWindow / 对话框 QDialog / 布局管理器 / 常用控件</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 3 / 4 个讲授日</p>
<ul>
<li>模块 A3 课时):Model/View 架构 / 事件机制</li>
<li>模块 B(3 课时):文件 IO / JSON 解析 → 本地数据存储案例</li>
<li>上午(08:3011:00 讲 / 11:0012:00 练):QMainWindow · 对话框 QDialog</li>
<li>下午(13:3016:00 讲 / 16:0017:00 作业):布局管理器 · 常用控件 · QSS 美化</li>
</ul>
<p><small>今天的主线:把昨天的计算器升级成"正经软件"——主窗口骨架 + 对话框交互 +
布局讲透 + 美化。</small></p>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<!-- ===================== 上午:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>Model/View 架构 / 事件机制 <span class="tag tag-lecture"></span></h2>
<p class="module-badge">上午 · 讲授 08:3011:00</p>
<h2>QMainWindow / 对话框 QDialog <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>Model/View:数据与展示分离 <span class="tag tag-lecture"></span></h3>
<h3>QMainWindow:标准桌面软件的骨架 <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>
<li>QMainWindow 不是"大号 QWidget":它有固定的区域布局协议——菜单栏 / 工具栏 /
状态栏 / 停靠窗(dock)各就其位,<b>中心部件必须 <code>setCentralWidget()</code>
显式安放</b>;菜单项和工具栏按钮的统一抽象是 <code>QAction</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>
<li>直接往 QMainWindow 上摆控件而不设中心部件——布局行为不可预期;</li>
<li><code>QAction</code> 显示出来 ≠ 生效:忘了 <code>connect</code> 到槽,
就是"菜单点了没反应"。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch05/central_widget.cpp">p03/ch05/central_widget.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch05/dock_widget.cpp">dock_widget.cpp</a></small></p>
</section>
<section>
<h3>Model/View 模型视图架构(教学补充) <span class="tag tag-lecture"></span></h3>
<h3>QDialog:模态与非模态 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><b>模态</b> <code>exec()</code>:阻塞等用户给结果,返回值就是用户的选择
Accepted/Rejected);<b>非模态</b> <code>show()</code>:立即返回,窗口独立存活
——所以对话框对象必须活过当前函数作用域(堆 + parent,或
<code>WA_DeleteOnClose</code> 自清理)。</li>
<li>标准对话框拿来就用:<code>QMessageBox</code>(提示/确认)、<code>QFileDialog</code>(选文件)。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑(昨天的所有权坑第三次出现!)</h4>
<ul>
<li><b>非模态对话框建在栈上</b><code>show()</code> 完函数一返回就析构——窗口一闪而过。
<code>p03/ch06</code> 的三个 modeless 版本(栈/堆/DeleteOnClose)就是这个坑的对照教材。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch06/modal_dialog.cpp">p03/ch06/modal_dialog.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch06/modeless_dialog_stack.cpp">modeless_dialog_stack.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch06/modeless_dialog_heap.cpp">modeless_dialog_heap.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch06/modeless_dialog_deleteonclose.cpp">modeless_dialog_deleteonclose.cpp</a></small></p>
</section>
<section>
<h3>标准对话框速览 <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 ret = QMessageBox::question(this, "确认", "真的要删除吗?");
if (ret == QMessageBox::Yes) { /* ... */ }
// 视图:只管展示,不持有数据
auto* view = new QListView(this);
view->setModel(model);
// 选中状态变化 → 通知外部,模型和视图之间没有直接耦合
connect(view->selectionModel(), &QItemSelectionModel::currentChanged, ...);
QString path = QFileDialog::getOpenFileName(this, "选择文件", "", "*.json");
if (!path.isEmpty()) { /* ... */ } // 用户可能点取消:必须处理空返回
</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>
<li>不处理"用户点了取消"(空路径/Rejected)就直接往下用——健壮性问题的高发点。</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>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch06/message_box_question.cpp">p03/ch06/message_box_question.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch06/message_box_custom.cpp">message_box_custom.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch06/file_dialog.cpp">file_dialog.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>
<p class="module-badge">上午 · 课堂练习 11:00–12:00(工程师指导、答疑)</p>
<h2>动手:计算器主窗口化 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · Model/View 换视图 <span class="tag tag-practice"></span></h4>
<h4>任务卡 1 · 给计算器套上 QMainWindow <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>
<dd>昨天的计算器改造为 QMainWindow 应用:加菜单「帮助→关于」弹
<code>QMessageBox</code>;加一个「历史」停靠窗(可先放空 QLabel)</dd>
<dt>验收标准</dt>
<dd>编译通过,界面正常展示同一份数据,证明"同一个模型可以喂给不同视图"</dd>
<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>
<dd><a class="filelink" target="_blank" href="../../../p03/ch05/central_widget.cpp">p03/ch05/central_widget.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch05/dock_widget.cpp">dock_widget.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch06/message_box_question.cpp">p03/ch06/message_box_question.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>
<p class="module-badge">下午 · 讲授 13:3016:00</p>
<h2>布局管理器 / 常用控件 / QSS 美化 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>QIODevice:统一的读写抽象 <span class="tag tag-lecture"></span></h3>
<h3>布局管理器(回收昨天的脚手架) <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>
<li>布局管理器接管子控件的位置/大小计算,随窗口缩放自动重排——昨天计算器里"先用后学"
<code>QGridLayout</code> 今天讲透:QHBox/QVBox/QGrid 三兄弟 + 嵌套组合 + 伸缩因子
<code>addStretch()</code>;布局对象同样遵循 QObject 父子所有权</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>忘记检查 <code>open()</code> 的返回值:文件不存在/无权限时静默失败,后续读写在一个
没打开的设备上操作,容易读出空数据而不报错。</li>
<li>混淆文本模式和二进制模式(<code>QIODevice::Text</code>):文本模式下 Qt 会做换行符
转换,用来读写二进制数据会破坏内容。</li>
<li>同一控件不能属于两个布局;</li>
<li>布局设置后又用 <code>setGeometry()</code>/<code>move()</code> 手动改位置——
会被下次布局刷新覆盖,看起来"设置没生效"。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch08/custom_widget.cpp">p03/ch08/custom_widget.cpp</a></small></p>
</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>
<h3>常用控件 + 资源系统 .qrc <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>
<li>QLabel 一个控件三种形态:纯文本 / 富文本 HTML / 图片(<code>QPixmap</code>/
动图(<code>QMovie</code>);输入靠 QLineEdit,按钮族 QPushButton/QRadioButton/QCheckBox。</li>
<li><code>:/...</code> 路径是 Qt 资源系统:资源编译期打包进可执行文件,运行时不依赖
外部文件——发布不用带一堆散图。</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>
<li><code>.qrc</code> 里的路径与代码引用不一致:<b>运行时静默失败</b>(图不显示),
不报编译错误;忘接 CMake 的 AUTORCC/<code>qt5_add_resources</code> 资源系统不生效</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch08/label_text_html.cpp">p03/ch08/label_text_html.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch08/label_pixmap.cpp">label_pixmap.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch08/label_movie.cpp">label_movie.cpp</a></small></p>
</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>
<h3>QSS 界面美化 <span class="tag tag-lecture"></span></h3>
<pre><code class="css" data-trim>QPushButton#okButton { background-color: #2f855a; border-radius: 6px; }
QPushButton#okButton:hover { background-color: #276749; }</code></pre>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>语法脱胎于 CSS:类型选择器 / ID 选择器(<code>#objectName</code>/ 伪状态
<code>:hover</code> <code>:pressed</code>)+ 层叠继承——作用对象是 QWidget 树而非 DOM
"长什么样"与"做什么"分离,改样式不碰逻辑。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>ID 选择器必须先 <code>setObjectName()</code></li>
<li><code>setStyleSheet()</code> 会级联影响<b>所有子控件</b>——范围失控是最容易踩的坑,
样式尽量写窄(带选择器),不要裸写属性。</li>
</ul>
</div>
<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>
href="../examples/qss_styling/qt_qss_styling.cpp">examples/qss_styling/qt_qss_styling.cpp</a>
<a class="filelink" target="_blank" href="../examples/qss_styling/README.md">README</a></small></p>
</section>
</section>
<!-- ===================== 模块 B:练(本地数据存储案例) ===================== -->
<!-- ===================== 下午:作业 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min(本地数据存储案例,练习任务,非预建方案</p>
<h2>动手:本地数据存储小工具 <span class="tag tag-practice"></span></h2>
<p class="module-badge">下午 · 课堂作业 16:00–17:00(工程师答疑</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>
<h4>任务卡 2 · 用 QSS 美化计算器 <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>
<dd>数字键/运算键/等号键用 QSS 区分颜色;整体布局改为随窗口缩放自适应</dd>
<dt>验收标准</dt>
<dd>程序运行后磁盘上生成一个 <code>.json</code> 文件,内容可读;重新运行程序能正确解析出
之前写入的数据</dd>
<dd>至少 2 类按钮有不同 <code>background-color</code>;至少 1 个按钮有
<code>:hover</code><code>:pressed</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>
<dd><a class="filelink" target="_blank" href="../examples/qss_styling/qt_qss_styling.cpp">examples/qss_styling/</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch08/custom_widget.cpp">p03/ch08/custom_widget.cpp</a></dd>
</dl>
</div>
</section>
@@ -321,13 +229,13 @@ qDebug() << obj["name"].toString() << obj["score"].toInt();
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>现场演示</h4>
<p>现场演示本地数据存储小工具,验证"写入 → 重启 → 读回"完整闭环</p>
<h4>验收</h4>
<p>现场演示升级后的计算器:主窗口(菜单/停靠窗)+ QSS 配色 + 缩放自适应,功能不回退</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> ·
<p class="footer-note"><small>钩子 · QSS 与前端 CSS 是同源思想;Qt Designer/QML 是另一条 UI
路线(拖拽/声明式),本课程走纯代码路线,知道它们存在即可。<br>
明天 Day4:事件机制 / 绘图 / 表格 + 文件系统——数据要开始落盘了。</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>
+152 -183
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 4 · 2D绘图/多线程/动画框架与状态机</title>
<title>Day 4 · 事件机制 / 绘图与表格 / 文件系统</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,278 +15,250 @@
<section>
<h1>Day 4</h1>
<h3>2D 图形绘制 / 多线程 / 动画框架与状态机</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 4 / 5 天 · 讲:练 ≈ 1:2.7</p>
<h3>Qt 消息机制和事件 / 绘图事件及表格事件 / 文件系统</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 4 / 4 个讲授日</p>
<ul>
<li>模块 A(3 课时):2D 绘图 / 多线程</li>
<li>模块 B(3 课时):动画框架 / 状态机 → 交互式小游戏案例</li>
<li>上午(08:3011:00 讲 / 11:0012:00 练):事件循环与派发 · 事件拦截 · 绘图事件 · 表格</li>
<li>下午(13:3016:00 讲 / 16:0017:00 作业):文件系统(QFile/QTextStream/QDataStream + JSON</li>
</ul>
<p><small>今晚的作业「本地数据存储小工具」是明天 Day5 综合案例的直接前身。</small></p>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<!-- ===================== 上午:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>2D 绘图 / 多线程 <span class="tag tag-lecture"></span></h2>
<p class="module-badge">上午 · 讲授 08:3011:00</p>
<h2>事件机制 / 绘图事件 / 表格事件 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>QPainter:由 Qt 驱动的重绘模型 <span class="tag tag-lecture"></span></h3>
<h3>事件循环与派发:事件 vs 信号 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>Qt 的绘制是"框架驱动"而非"你想画就画":所有绘制只能发生在 <code>paintEvent</code>
里,由 Qt 在需要重绘时(窗口刚显示、被遮挡后重新露出、你主动请求)自动调用——这和直接式
绘图(想画就立刻画到屏幕)是不同的模型</li>
<li>控件想主动触发重绘要调用 <code>update()</code>(异步,排队到下次事件循环处理)而不是
直接调用 <code>paintEvent()</code><code>QPainter</code> 只是"在哪个设备上画"的画笔,
画布可以是控件本身,也可以是 <code>QImage</code>/<code>QPixmap</code> 等离屏设备。</li>
<li>Day2 的 <code>app.exec()</code> 今天拆开看:事件循环不停从队列取 <code>QEvent</code>
派发给目标控件——<b>事件</b>是框架推给控件的通知(鼠标/键盘/重绘),<b>信号槽</b>
对象间的主动通知,两套机制并存不冲突</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑(明天的伏笔)</h4>
<ul>
<li><b>主线程做耗时操作 = 事件循环停摆 = 整个界面冻结</b>——点不动、拖不动、白屏。
这是学员项目里最高频的"卡死"来源,正确解法(多线程)明天综合案例专门讲。</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch02/minimal_qt_app.cpp">p03/ch02/minimal_qt_app.cpp</a><code>exec()</code></small></p>
</section>
<section>
<h3>事件拦截:重写 xxxEvent vs 事件过滤器 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>两条拦截路径:重写 <code>mousePressEvent()</code> 等虚函数(只影响自己);
<code>installEventFilter()</code>(一个对象统一拦截多个控件的事件)。
事件有 <code>accept()</code>/<code>ignore()</code> 状态,决定是否继续向父控件传播。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li><code>paintEvent</code> 之外的地方直接 <code>new QPainter(this)</code> 画东西:
要么无效要么未定义行为,绘制必须在 <code>paintEvent</code> 回调期间进行</li>
<li>数据变了却忘记调用 <code>update()</code>:界面不会自动感知"要重画了",看起来像是
"画错了",其实是"根本没重画"。</li>
<li>重写事件函数忘记调用基类实现(<code>QWidget::mousePressEvent(e)</code>)——
控件默认行为悄悄丢失,现象诡异难查</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch09/event_override_customwidget.cpp">p03/ch09/event_override_customwidget.cpp</a> ·
<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></small></p>
</section>
<section>
<h3>绘图:QPainter <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
void paintEvent(QPaintEvent*) override {
QPainter painter(this);
painter.drawLine(QLine(10, 10, 200, 10));
painter.drawRect(QRect(10, 30, 100, 50));
painter.drawEllipse(QRect(150, 30, 100, 80));
}
</code></pre>
<p><small>所有绘制都发生在 <code>paintEvent</code> 里,由 Qt 在需要重绘时自动调用。
出处:<a class="filelink" target="_blank"
href="../../../p03/ch10/qpainter_basic.cpp">p03/ch10/qpainter_basic.cpp</a>;绘图设备
<code>QImage</code>/<code>QPixmap</code>/<code>QPicture</code>)见
<a class="filelink" target="_blank" href="../../../p03/ch10/paint_device_qimage.cpp">paint_device_qimage.cpp</a>
<a class="filelink" target="_blank" href="../../../p03/ch10/paint_device_pixmaps.cpp">paint_device_pixmaps.cpp</a></small></p>
</section>
<section>
<h3>QThread:线程亲和性与 worker-object <span class="tag tag-lecture"></span></h3>
<h3>绘图事件QPainter <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>"线程亲和性"thread affinity):每个 QObject 都归属于创建它/被
<code>moveToThread</code> 移动到的那个线程,它的槽函数默认在所属线程执行——GUI 控件必须
始终归属主线程,这是 Qt 线程模型的核心约束</li>
<li>官方推荐的是 worker-object 模式(把耗时逻辑放进一个普通 QObject,
<code>moveToThread</code> 到工作线程),而不是继承 <code>QThread</code> 重写
<code>run()</code>——前者能继续用信号槽通信,后者容易把线程亲和性搞混。</li>
<li>跨线程的信号槽连接会自动变成 Queued(回顾
<a class="filelink" target="_blank" href="day2.html">Day2</a>),结果通过事件循环
安全地"带回"接收者所在线程,不需要手动加锁。</li>
<li>Qt 绘制是<b>框架驱动</b>而非"想画就画":所有绘制只能发生在 <code>paintEvent()</code>
里,由 Qt 按需调用;数据变了想重画,调 <code>update()</code>(异步排队一次重绘请求)。
绘图设备三件套 QImage/QPixmap/QPicture 作参考</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>工作线程里直接操作 GUI 控件(比如直接 <code>label->setText()</code>):Qt 的 GUI
模块不是线程安全的,这是未定义行为/崩溃的常见来源——正确做法永远是发信号,排队回主线程处理</li>
<li><code>moveToThread</code> 要在 <code>connect</code><code>thread.start()</code>
之前完成;对象一旦有耗时任务在跑,中途再挪线程会有竞态。</li>
<li> <code>paintEvent</code> 之外直接构造 QPainter 画窗口——未定义行为;</li>
<li>数据变了忘调 <code>update()</code>——界面不会自动重画</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch10/qpainter_basic.cpp">p03/ch10/qpainter_basic.cpp</a>
(设备类参考:<a class="filelink" target="_blank" href="../../../p03/ch10/paint_device_qimage.cpp">paint_device_qimage.cpp</a> 等)</small></p>
</section>
<section>
<h3>QThread 多线程(教学补充) <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
class Worker : public QObject {
Q_OBJECT
public slots:
void process() {
qint64 sum = 0;
for (int i = 1; i <= 1000000; ++i) sum += i;
emit resultReady(sum); // 排队连接自动把结果带回主线程
}
signals:
void resultReady(qint64 result);
};
worker.moveToThread(&workerThread); // 官方推荐写法:worker-object + moveToThread
connect(&workerThread, &QThread::started, &worker, &Worker::process);
</code></pre>
<h3>表格:QTableWidget 与单元格事件 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>item 类控件:数据直接塞进控件(<code>QTableWidgetItem</code>new 出来交给表格
接管所有权),上手快、中小项目够用;"表格事件"以信号暴露:
<code>cellClicked</code> / <code>cellDoubleClicked</code> / <code>itemChanged</code></li>
</ul>
</div>
<div class="concept-box">
<h4>钩子 · 它背后是完整的 Model/View 架构</h4>
<ul>
<li>数据(model)与展示(view)彻底分离、同一模型可喂多个视图(QListView/QTableView);
改模型必须发 <code>dataChanged</code> 等信号视图才会刷新。数据规模大/多视图联动时的
正解,自学入口:<a class="filelink" target="_blank"
href="../examples/model_view/qt_model_view_basics.cpp">examples/model_view/</a></li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../examples/qthread_worker/qt_thread_worker.cpp">docs/teaching/examples/qthread_worker/qt_thread_worker.cpp</a>
<a class="filelink" target="_blank" href="../examples/qthread_worker/README.md">配套 README</a>——
仓库 <code>p03/</code> 的 wiki 抓取内容没有多线程,这是本轮补写的教学补充示例,已验证跑通。</small></p>
href="../examples/table_widget/qt_table_widget_basics.cpp">examples/table_widget/qt_table_widget_basics.cpp</a>
<a class="filelink" target="_blank" href="../examples/table_widget/README.md">README</a></small></p>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<!-- ===================== 上午:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:绘图 + 多线程 <span class="tag tag-practice"></span></h2>
<p class="module-badge">上午 · 课堂练习 11:00–12:00(工程师指导、答疑)</p>
<h2>动手:拦事件 + 画图形 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 画一个简单图形界面 <span class="tag tag-practice"></span></h4>
<h4>任务卡 1 · 事件过滤器 + 自绘图形 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>仿 <code>qpainter_basic.cpp</code>,在 <code>paintEvent</code> 里画一个组合图形
棋盘格/仪表盘刻度)</dd>
<dd>① 给一个控件装事件过滤器,拦截鼠标按下并打印坐标;
② 在 <code>paintEvent</code> 里画一个组合图形(棋盘格仪表盘刻度)</dd>
<dt>验收标准</dt>
<dd>编译通过,离屏或 X 环境下能看到图形正确绘制(可截图验证)</dd>
<dd>点击控件终端能看到坐标输出;图形正确绘制(可截图验证)</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch10/qpainter_basic.cpp">p03/ch10/qpainter_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><code>qt_thread_worker.cpp</code> 里的耗时计算换成一个更长的循环或
<code>QThread::msleep</code>,观察主线程/事件循环没有被卡住</dd>
<dt>验收标准</dt>
<dd>能说清楚"如果把这段计算直接放主线程会发生什么"(界面无响应),并解释
<code>moveToThread</code> 起的作用</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../examples/qthread_worker/qt_thread_worker.cpp">qt_thread_worker.cpp</a> ·
<a class="filelink" target="_blank" href="../examples/qthread_worker/README.md">README</a></dd>
<dd><a class="filelink" target="_blank" href="../../../p03/ch09/event_filter_mainwindow.cpp">p03/ch09/event_filter_mainwindow.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch10/qpainter_basic.cpp">p03/ch10/qpainter_basic.cpp</a></dd>
</dl>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<!-- ===================== 下午:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>动画框架 / 状态机 <span class="tag tag-lecture"></span></h2>
<p class="module-badge">下午 · 讲授 13:3016:00</p>
<h2>文件系统:QFile / 流 / JSON <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>QPropertyAnimation:动画建立在属性系统上 <span class="tag tag-lecture"></span></h3>
<h3>QIODevice 统一读写抽象 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>Qt 的属性系统(<code>Q_PROPERTY</code>,同样靠
<a class="filelink" target="_blank" href="day2.html">Day2</a> 讲过的 moc 生成)给每个属性配了
统一的 get/set 接口;动画框架的本质就是"在一段时间内,用缓动曲线不断调用某个属性的
setter",所以任何声明成 <code>Q_PROPERTY</code> 的属性(不只是内置的 geometry/opacity
都能被动画驱动。</li>
<li><code>QEasingCurve</code> 决定的是"数值随时间变化的曲线形状"(线性/回弹/加减速……),
动画本身只是"定时器 + 插值 + 调 setter"的组合,理解这点能解释为什么几乎所有属性都能做动画。</li>
<li><code>QFile</code>/<code>QTextStream</code>/<code>QDataStream</code> 都建立在
<code>QIODevice</code> 统一接口上——之后遇到网络/串口也是同一套;
<code>QTextStream</code> 面向文本行,<code>QDataStream</code> 面向结构化二进制。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>要动画化的属性必须是真正的 <code>Q_PROPERTY</code>(有 setter),随手传一个字符串
属性名如果对应类没有声明该属性,动画不会报错,只是什么也不会发生。</li>
<li>动画作用的对象在动画播放完成前被销毁会导致悬空访问;
<code>QAbstractAnimation::DeleteWhenStopped</code> 能让动画结束后自动释放自身,但不管
目标对象的生命周期,目标对象仍需自己保证不提前销毁。</li>
<li><code>open()</code> 返回值不检查——文件不存在/没权限时<b>静默失败</b>
后续读写全是空操作;</li>
<li>文本/二进制模式混淆会破坏内容。</li>
</ul>
</div>
<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>QPropertyAnimation 动画(教学补充) <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
auto* anim = new QPropertyAnimation(button, "geometry", &window);
anim->setDuration(800);
anim->setStartValue(QRect(10, 10, 100, 30));
anim->setEndValue(QRect(180, 140, 120, 40));
anim->setEasingCurve(QEasingCurve::OutBounce);
anim->start(QAbstractAnimation::DeleteWhenStopped);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../examples/property_animation/qt_property_animation.cpp">docs/teaching/examples/property_animation/qt_property_animation.cpp</a>
<a class="filelink" target="_blank" href="../examples/property_animation/README.md">配套 README</a></small></p>
</section>
<section>
<h3>QStateMachine:状态机建立在信号槽上 <span class="tag tag-lecture"></span></h3>
<h3>JSON 解析与写入 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>状态机解决的是"同一个控件在不同状态下行为/外观不同"的问题——与其用一堆
<code>bool</code> 标志位加 if-else 判断当前处于什么状态,不如让"状态"本身成为一等对象
<code>QState</code>),每个状态自己声明"进入这个状态要怎样"(<code>assignProperty</code>
又用到了 Qt 属性系统)。</li>
<li>状态之间的切换(<code>addTransition</code>)本质是"信号触发的转移"——转移条件就是
一个信号,这让状态机和信号槽自然衔接:任何能发信号的事件都能驱动状态切换。</li>
<li>序列化/反序列化是一对互逆操作,正确性标准是 <b>round-trip</b>:写出去再读回来
数据一致;<code>QJsonDocument/Object/Array/Value</code> 四件套,
<code>QJsonValue</code> 是能装多种类型的变体。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>忘记 <code>machine.setInitialState(...)</code> 或忘记调用 <code>machine.start()</code>
状态机对象存在但从未真正运行,所有 <code>assignProperty</code>/<code>addTransition</code>
都不会生效</li>
<li>某个状态没有覆盖所有可能触发的信号/条件,会导致状态机"卡住"——设计状态机前先把所有
状态和转移条件画成图,比直接写代码更不容易漏。</li>
<li>不检查 <code>QJsonParseError</code>:格式坏的输入<b>静默变成空文档</b></li>
<li>类型不匹配时 <code>toInt()</code>/<code>toString()</code> 不报错只返回默认值——
数据悄悄丢,很难查</li>
</ul>
</div>
<p><small>出处:<a class="filelink" target="_blank"
href="../examples/json_io/qt_json_parse_write.cpp">examples/json_io/qt_json_parse_write.cpp</a>
<a class="filelink" target="_blank" href="../examples/json_io/README.md">README</a></small></p>
</section>
<section>
<h3>★ 相对路径与工作目录 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
QFile bad("data.json"); // ✗ 相对当前工作目录——它在哪取决于你怎么启动
QFile good(QCoreApplication::applicationDirPath() + "/data.json"); // ✓ 锚定可执行文件位置
</code></pre>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>相对路径基于<b>进程当前工作目录</b>——不是源码目录,也不一定是可执行文件目录;
IDE、终端、脚本三种启动方式的工作目录可能各不相同。正解:
<code>applicationDirPath()</code> 拼路径,或只读资源走 <code>.qrc</code>(昨天的资源卡)。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>"IDE 里能读到文件,命令行跑就说不存在"——几乎每届学员必问,根源就是这页。</li>
</ul>
</div>
</section>
<section>
<h3>QStateMachine 状态机(教学补充 <span class="tag tag-lecture"></span></h3>
<h3>★ 文本编码再遇(文件版 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
auto* redState = new QState(&machine);
auto* greenState = new QState(&machine);
redState->assignProperty(&button, "text", QStringLiteral("红灯:禁止通行"));
greenState->assignProperty(&button, "text", QStringLiteral("绿灯:允许通行"));
redState->addTransition(&button, &QPushButton::clicked, greenState);
greenState->addTransition(&button, &QPushButton::clicked, redState);
machine.setInitialState(redState);
machine.start();
QTextStream ts(&file);
ts.setCodec("UTF-8"); // Qt 5 默认编码随平台/区域设置而变——跨机器必须显式指定
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../examples/state_machine/qt_state_machine.cpp">docs/teaching/examples/state_machine/qt_state_machine.cpp</a>
<a class="filelink" target="_blank" href="../examples/state_machine/README.md">配套 README</a>)——
<code>QStateMachine</code>/<code>QState</code> 属于 QtCore,不需要额外模块。以上两个
(动画、状态机)都是仓库 <code>p03/</code> 原本没有的教学补充示例,已离屏验证跑通。</small></p>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>Day2 界面乱码(QString/源码编码)和今天文件乱码是<b>同一根源</b>的两次出现:
字节 ↔ 文字之间永远隔着一层"编码",跨边界(源码/文件/网络)就必须显式指定。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>Windows 记事本写的文件 Linux 程序读出乱码(或反过来)——不是"文件坏了",
是两边默认编码不同;养成条件反射:<b>编码要显式指定</b></li>
</ul>
</div>
</section>
</section>
<!-- ===================== 模块 B:练(交互式小游戏案例,选做) ===================== -->
<!-- ===================== 下午:作业 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min(交互式小游戏案例,练习任务,选做,非预建方案</p>
<h2>动手:动画 + 状态机小游戏 <span class="tag tag-practice"></span></h2>
<p class="module-badge">下午 · 课堂作业 16:00–17:00(工程师答疑</p>
<h2>作业:本地数据存储小工具 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 动画 + 状态机组合 <span class="tag tag-practice"></span></h4>
<h4>任务卡 2 · 本地数据存储小工具 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>把动画和状态机组合到同一个窗口——例如按钮在"红灯"状态时禁止移动,"绿灯"状态时
点击才触发动画</dd>
<dd>设计「学员信息」结构(姓名/成绩/标签),支持增加记录并保存到 JSON 文件,
重启程序后能读出之前保存的全部记录</dd>
<dt>验收标准</dt>
<dd>状态切换正确(红灯不触发动画,绿灯触发动画);动画播放完整不报错</dd>
<dd>至少能存 2 条以上记录并在重启后完整读回;字段不丢失;数据文件路径用
<code>applicationDirPath()</code> 拼(今天的坑卡现学现用)</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../examples/property_animation/">property_animation/</a> ·
<a class="filelink" target="_blank" href="../examples/state_machine/">state_machine/</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>结合任务卡 1 + 鼠标事件(Day3 <code>mouse_event.cpp</code>+ 多线程计时
Day4 <code>qt_thread_worker.cpp</code>),做一个"状态变化后限时点击"的最小交互游戏</dd>
<dt>验收标准</dt>
<dd>游戏至少有"开始→进行中→结束"三个可辨识状态,计时或计分逻辑不崩溃</dd>
<dt>基础知识 · 参考</dt>
<dd>上述三个教学补充示例 +
<a class="filelink" target="_blank" href="../../../p03/ch09/mouse_event.cpp">mouse_event.cpp</a></dd>
<dd><a class="filelink" target="_blank" href="../examples/json_io/qt_json_parse_write.cpp">examples/json_io/</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch11/qfile_basic.cpp">p03/ch11/qfile_basic.cpp</a></dd>
</dl>
</div>
</section>
@@ -296,15 +268,12 @@ machine.start();
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>现场演示</h4>
<pre><code class="bash" data-trim>QT_QPA_PLATFORM=offscreen tools/run_qt.sh &lt;target&gt;
# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口</code></pre>
<p>分组演示动画+状态机组合小工具(任务卡 2 选做,量力而行)。</p>
<h4>验收</h4>
<p>现场演示「写入 → 重启 → 读回」完整闭环;讲得出 open()/ParseError 两处检查为什么不能省。</p>
</div>
<p class="footer-note"><small>明天 Day5:团队协同开发 / 综合案例复盘 / OpenCV(可选自学) ——
大纲:<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="day3.html">← 上一天</a> ·
<p class="footer-note"><small>明天 Day5 全天综合项目案例:学员信息管理器——今天的作业就是
它的雏形;还要补上最后一块拼图:多线程防卡界面。</small></p>
<p><a class="filelink" href="index.html">返回目录</a> · <a class="filelink" href="day3.html">← 上一天</a> ·
<a class="filelink" href="day5.html">下一天 →</a></p>
</section>
+161 -155
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 5 · 团队协同开发/综合案例复盘/OpenCV可选</title>
<title>Day 5 · 综合项目案例讲解:学员信息管理器</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,198 +15,204 @@
<section>
<h1>Day 5</h1>
<h3>团队协同开发 / 综合案例复盘 / OpenCV(可选自学</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 5 / 5 天 · 最后一天课堂内容</p>
<h3>综合项目案例讲解:学员信息管理器(全天</h3>
<p class="module-badge">C++/Qt 方向实训 · 综合案例日 · 明天起进入项目开发</p>
<ul>
<li>模块 A(3 课时):团队协同开发(概念讲解</li>
<li>模块 B(3 课时):综合案例复盘 + OpenCV/AI(可选,自学指针)</li>
<li>上午:需求拆解 → 界面骨架 → 数据层(Day1–4 全部知识点的收束</li>
<li>下午:提升——QThread 防卡界面;开发流程与规范;分组定项目方向</li>
</ul>
<p><small>今天结束后 Day6–10 是纯项目实作(学员编码 + 教员指导 + 成果展示),
不再有课堂讲授——今天的分组结论会直接带到项目实作阶段。</small></p>
<p><small>目标:通过一个完整小软件讲清 Qt 开发的<b>流程、规范及方法</b>——
项目 = 已学知识点的组装。参考实现:
<a class="filelink" target="_blank" href="../examples/student_manager/qt_student_manager.cpp">examples/student_manager/</a>
<a class="filelink" target="_blank" href="../examples/student_manager/README.md">README</a>)。
Day6–9 学员项目开发,Day10 下午答辩。</small></p>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<!-- ===================== 上午 1:需求与骨架 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>团队协同开发 <span class="tag tag-lecture"></span></h2>
<p class="module-badge">上午 1 · 需求拆解 → 界面骨架</p>
<h2>从需求到窗口 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>质量保证:四平台一标准(复习) <span class="tag tag-lecture"></span></h3>
<img src="../../images/quality_four_platforms.png" style="max-height:520px;">
<p><small>出处:<a class="filelink" target="_blank"
href="../../TRAINING_PLAN_2026.md">docs/TRAINING_PLAN_2026.md</a> 「七、质量保证 (二) Git」</small></p>
<h3>需求拆解:先想清楚再动手 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念——开发流程的第一步不是写代码</h4>
<ul>
<li>需求:管理学员记录(姓名/成绩/标签),支持添加、保存、读取、批量导入。</li>
<li>拆功能 → 映射到已学组件:列表展示=QTableWidget(Day4)、添加=模态对话框(Day3)、
保存读取=QFile+JSON(Day4)、批量导入=后台线程(今天新授)、外壳=QMainWindow(Day3)。</li>
<li>拆完就得到任务清单——这正是 Day6 起你们在 Jira 上要做的事。</li>
</ul>
</div>
</section>
<section>
<h3>Git 提交习惯:核心概念 <span class="tag tag-lecture"></span></h3>
<h3>界面骨架走查 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>回指 Day34</h4>
<ul>
<li>QMainWindow:菜单(文件/记录/帮助)+ 工具栏 + 状态栏 + <code>setCentralWidget(table)</code></li>
<li>QAction 一个动作三处复用(菜单/工具栏/快捷键),<b>每个 action 都要 connect</b></li>
<li>添加记录:模态 <code>exec()</code> + <code>QFormLayout</code> + <code>QDialogButtonBox</code></li>
<li>表格 item 所有权:new 出来交给表格——父子所有权模型第 N 次出现。</li>
</ul>
</div>
<p><small>现场跑:<code>tools/run_qt.sh qt_student_manager</code>(离屏自测版:
<code>QT_QPA_PLATFORM=offscreen</code>,自动走完整验证流程)</small></p>
</section>
</section>
<!-- ===================== 上午 2:数据层 ===================== -->
<section>
<section>
<p class="module-badge">上午 2 · 数据层</p>
<h2>记录 ↔ JSON ↔ 磁盘 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>数据层走查 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
struct Student { QString name; double score; QString tag; };
QJsonObject toJson(const Student&); // 互逆的一对
Student fromJson(const QJsonObject&); // round-trip = 正确性标准
</code></pre>
<div class="concept-box">
<h4>回指 Day4(昨天作业的完整版)</h4>
<ul>
<li>数据结构与界面分离:<code>Student</code> 结构体不知道 QTableWidget 的存在——
这是 Model/View 思想的朴素版;</li>
<li>健壮性三处检查全部在场:<code>open()</code> 返回值、<code>QJsonParseError</code>
路径用 <code>applicationDirPath()</code> 拼。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>复盘昨天作业的常见问题</h4>
<ul>
<li>保存和读取的字段名拼写不一致("score" vs "Score")——round-trip 一测就露馅,
这就是为什么验收标准是"写进去读出来一致"而不是"能生成文件"。</li>
</ul>
</div>
</section>
</section>
<!-- ===================== 下午 1QThread 提升 ===================== -->
<section>
<section>
<p class="module-badge">下午 1 · 提升:多线程防卡界面(新授)</p>
<h2>QThread:耗时操作不冻结界面 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>先看病症:把耗时活儿放主线程 <span class="tag tag-lecture"></span></h3>
<div class="pitfall-box">
<h4>现场演示</h4>
<ul>
<li><code>msleep</code> 循环直接放按钮槽里 → 整个窗口冻结:点不动、拖不动、
系统提示"未响应"——Day4 讲过:<b>主线程阻塞 = 事件循环停摆</b>
这是学员项目里最高频的"卡死"来源。</li>
</ul>
</div>
</section>
<section>
<h3>正解:worker-object 模式 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
auto* thread = new QThread(this);
auto* worker = new ImportWorker; // 不给 parent:有 parent 禁止 moveToThread
worker->moveToThread(thread); // 必须在 start() 之前
connect(thread, &QThread::started, worker, [worker]{ worker->doImport(10); });
connect(worker, &ImportWorker::rowReady, this, /* 更新表格 */); // 自动 Queued
thread->start();
</code></pre>
<div class="concept-box">
<h4>核心概念(三件套)</h4>
<ul>
<li><b>线程亲和性</b>:每个 QObject 归属一个线程,GUI 控件必须归主线程;</li>
<li><b>worker 对象</b>:官方推荐做法——耗时逻辑放 worker,<code>moveToThread</code>
搬进工作线程,而不是继承 QThread 重写 run</li>
<li><b>跨线程信号槽自动 Queued</b>(Day2 的连接方式今天闭环):worker 发信号、
主线程槽更新表格,不需要手动加锁。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>在工作线程直接操作 GUI 控件——未定义行为,跨线程崩溃的头号来源;</li>
<li><code>moveToThread</code> 写在 <code>start()</code> 之后 / worker 带了 parent——都不生效。</li>
</ul>
</div>
<p><small>最小版:<a class="filelink" target="_blank"
href="../examples/qthread_worker/qt_thread_worker.cpp">examples/qthread_worker/</a> ·
案例集成版:<a class="filelink" target="_blank"
href="../examples/student_manager/qt_student_manager.cpp">student_manager 的 startImport()</a></small></p>
</section>
</section>
<!-- ===================== 下午 2:流程与规范 ===================== -->
<section>
<section>
<p class="module-badge">下午 2 · 开发流程与规范 + 分组定方向</p>
<h2>进项目前的最后一课 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>Git 提交习惯 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>接下来 Day6–10 项目实作,每人每完成一个任务都要提交代码到 Git 并写清任务描述——
提交粒度对应 Jira 任务粒度,commit 信息就是"这次提交做了什么、为什么"的可追溯记录</li>
<li>每完成一个任务就提交,提交粒度对应任务粒度;commit 信息说清"做了什么、为什么"——
它是项目的可追溯记录,评审和回滚都靠它</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见误区</h4>
<ul>
<li>几天代码一次性提交、commit 信息写"update":历史记录失去可追溯性,出问题无法
定位是哪次改动引入的。</li>
<li>攒几天代码一次性提交、信息写"update"——历史失去可追溯性,出问题无法定位是哪次
改动引入的。从 Day6 的第一次 commit 开始就按规范来</li>
</ul>
</div>
</section>
<section>
<h3>标准化考核标准 <span class="tag tag-lecture"></span></h3>
<img src="../../images/assessment_standard.png" style="max-height:520px;">
<p><small>出处:<a class="filelink" target="_blank"
href="../../TRAINING_PLAN_2026.md">docs/TRAINING_PLAN_2026.md</a> 「七、(五) 标准化的考核标准」</small></p>
</section>
<section>
<h3>考核标准:核心概念 <span class="tag tag-lecture"></span></h3>
<h3>考核标准与分组定方向 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>项目管理评估(40%) + 项目技术评估(30%) + 职业素养评估(30%)——技术实现只占三成,
Day6–10 项目实作最终按这三个维度综合打分,不是"代码能跑就满分"</li>
<li>考核不只看代码:项目管理(任务拆解/进度可见)+ 项目技术 + 职业素养(协作/答辩表达)
多维度评估——只埋头写代码会丢掉其他维度的分</li>
<li>今天散场前每组确认:项目选题 + 初步分工 + 任务拆解草稿(上午"需求拆解"那页
就是模板)。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见误区</h4>
<ul>
<li>只顾埋头写代码、忽略项目管理和职业素养维度(如任务跟踪是否及时更新、沟通是否
到位):技术分再高,另外 70% 的权重会明显拉低总评。</li>
</ul>
</div>
</section>
</section>
<!-- ===================== 模块 A:讨论 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讨论 95min</p>
<h2>动手:分工复盘 <span class="tag tag-discuss"></span></h2>
</section>
<section>
<div class="task-card discuss-card">
<h4>任务卡 1 · 分工复盘 <span class="tag tag-discuss"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>结合考核标准图(项目管理评估/项目技术评估/职业素养评估三大块),对照 Day1
任务卡 1 定下的分工,检查执行情况</dd>
<dt>验收标准</dt>
<dd>每组能说出至少 1 项做得好的、1 项需要改进的</dd>
<dt>基础知识 · 参考</dt>
<dd>上一节"标准化考核标准"图 · Day1 任务卡 1 的分工结论</dd>
</dl>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>综合案例复盘 + OpenCV/AI(可选) <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>Day24 案例回顾 <span class="tag tag-lecture"></span></h3>
<p>请 1–2 组现场展示计算器 / 本地数据存储 / 动画状态机小游戏,其他组观摩提问。</p>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>代码走查(code review)是团队协作的常规环节——展示时讲清"为什么这么设计"比"功能能跑"
更能体现对 Day2–4 核心概念(对象树/信号槽/Model-View/线程亲和性……)的理解程度。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见误区</h4>
<ul>
<li>只演示"能跑"不讲"为什么":观摩组提不出有效问题,复盘就失去了巩固概念的作用。</li>
</ul>
</div>
</section>
<section>
<h3>OpenCV 与 AI 机器学习(可选,自学指针) <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>OpenCV 是独立于 Qt 的计算机视觉库,Qt 负责界面/交互,OpenCV 负责图像算法——两者通过
"内存里的图像数据格式转换"(<code>cv::Mat</code><code>QImage</code>)配合,是很多
桌面视觉类项目的典型组合方式。</li>
</ul>
</div>
<div class="verify-box">
<h4>缺口说明</h4>
<p>方案书原文对本项标注"可选择",属于非必修拓展。引入 OpenCV 会新增一条完整的
三方库工具链,本仓库暂不提供可运行示例。如果感兴趣,自学方向是
<code>cv::CascadeClassifier</code> 人脸检测入门。</p>
</div>
<p><small><span class="ext-ref">OpenCV 官方教程(doc.opencv.org,选读·需联网)</span></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 · 三选一深化 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>从计算器 / 本地数据存储 / 动画状态机小游戏中选一个继续深化(加一个新功能)</dd>
<dt>验收标准</dt>
<dd>现场演示新增功能正常工作,编译无报错</dd>
<dt>基础知识 · 参考</dt>
<dd>回顾 Day2–4 对应的教学补充示例与 README:
<a class="filelink" target="_blank" href="../examples/qss_styling/README.md">qss_styling</a> ·
<a class="filelink" target="_blank" href="../examples/json_io/README.md">json_io</a> ·
<a class="filelink" target="_blank" href="../examples/property_animation/README.md">property_animation</a> ·
<a class="filelink" target="_blank" href="../examples/state_machine/README.md">state_machine</a></dd>
</dl>
<div class="verify-box" style="font-size:0.55em;">
<h4>兴趣钩子(各 5 分钟演示,选学)</h4>
<p>给界面加动效:<a class="filelink" target="_blank"
href="../examples/property_animation/qt_property_animation.cpp">QPropertyAnimation</a>(任何
Q_PROPERTY 都能被缓动曲线驱动) ·
多状态 UI<a class="filelink" target="_blank"
href="../examples/state_machine/qt_state_machine.cpp">QStateMachine</a>(状态成为一等对象,
替代一堆 bool 标志)· 线程的更高抽象:QtConcurrent/线程池。</p>
</div>
</section>
</section>
<!-- ===================== 收尾 ===================== -->
<section>
<h2>当日产出验证 · Day15 阶段总验收</h2>
<h2>当日产出验证 · 进入项目期</h2>
<div class="verify-box">
<h4>现场演示</h4>
<p>每组现场演示深化后的案例,教师按考核标准图三大块给出反馈,为 Day6–10
的正式项目实作分组定方向。</p>
<h4>验收</h4>
<pre><code class="bash" data-trim>QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_student_manager
# 自测 1/2:JSON 写入→清空→读回,2 条记录完整
# 自测 2/2:后台线程导入 3 条,总计 5 条,界面未阻塞</code></pre>
<p>每组提交:项目选题 + 分工说明 + 任务拆解草稿。明天 Day6 起:学员编码 + 工程师指导,
Day10 下午项目答辩。</p>
</div>
<p class="footer-note"><small>Day1–5 课堂教学到此结束,Day6–10 进入项目实作——大纲
<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="day4.html">← 上一天</a></p>
</section>
<!-- ===================== 延伸阅读(全教案最末页) ===================== -->
<section>
<h2>延伸阅读 · Day6–10 项目实作支撑知识体系</h2>
<div class="verify-box">
<h4>16 个可选项目,除 C++/Qt 之外还需要哪些知识?</h4>
<p>系统梳理了实现这 16 个项目所需的 12 个辅助专业领域——数据结构与算法、
2D 游戏与碰撞、计算机图形学、数字图像处理、计算机视觉与机器学习(OpenCV/Haar/LBPH)、
编译原理与状态机、编码理论、计算机网络、并发编程、文件系统与增量同步、软件设计模式等,
<strong>每个核心知识点附 2–3 个已验证可用的网络课程链接</strong>(中英文兼顾)。</p>
<p>全文(serve.py 下本地一点即开):<a class="filelink" target="_blank"
href="../SUPPORTING_KNOWLEDGE.md">docs/teaching/SUPPORTING_KNOWLEDGE.md
&nbsp;&nbsp; qt-course-projs/docs/supporting-knowledge.md</a></p>
</div>
<p class="footer-note"><small>文档由项目仓库 <code>qt-course-projs</code> 维护,
本仓库以相对符号链接引用,单一源头、不复制;如直接用文件协议打开本页,请改用
<code>python3 docs/teaching/serve.py</code> 起本地服务以渲染该 Markdown。</small></p>
<p><a class="filelink" href="index.html">返回目录</a> ·
<a class="filelink" href="day4.html">← 上一天</a></p>
<p class="footer-note"><small>案例参考实现与讲解建议
<a class="filelink" target="_blank" href="../examples/student_manager/README.md">examples/student_manager/README.md</a> ·
大纲:<a class="filelink" href="../OUTLINE_4DAY.md">docs/teaching/OUTLINE_4DAY.md</a> §3</small></p>
<p><a class="filelink" href="index.html">返回目录</a> · <a class="filelink" href="day4.html">← 上一天</a></p>
</section>
</div>
+19 -18
View File
@@ -2,7 +2,7 @@
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>C++/Qt 方向实训 · 5 天教案幻灯片</title>
<title>C++/Qt 方向实训 · 4+1 天教案幻灯片</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: -apple-system, "Helvetica Neue", "PingFang SC", "Microsoft YaHei", sans-serif;
@@ -17,24 +17,25 @@
</style>
</head>
<body>
<h1>川大 C++/LinuxC 课程「02. Qt 方向」—— 5 天实训幻灯片</h1>
<p class="desc">对齐官方《四川大学软件学院 2026 年夏 C/C++ 项目实习实训方案书 V1.2》
「五、执行计划 (二) C++方向」Day1–5(Day6–10 是纯项目实作,无课堂内容,不在本教案范围)。
配套大纲:<a href="../OUTLINE.md"><code>docs/teaching/OUTLINE.md</code></a>
方案书全文:<a href="../../TRAINING_PLAN_2026.md"><code>docs/TRAINING_PLAN_2026.md</code></a>
离线可用(reveal.js 已 vendor 进仓库);代码链接需通过本地 HTTP 服务打开才能正常跳转,
见仓库 <a href="../OUTLINE.md"><code>docs/teaching/OUTLINE.md</code></a> 「材料与播放方式」一节。</p>
<h1>川大 C++/LinuxC 课程「02. Qt 方向」—— 讲授 4 天 + 综合案例 1 天</h1>
<p class="desc">对齐 <b>2026-07 新版 10 天实训安排</b>(Day1–4 讲授、Day5 综合案例讲解、
Day6–10 学员项目开发与答辩)。配套大纲:
<a href="../OUTLINE_4DAY.md"><code>docs/teaching/OUTLINE_4DAY.md</code></a>
(含新安排简表、逐天设计与中初级学员适配性评估);
旧 5 天版大纲 <a href="../OUTLINE.md"><code>OUTLINE.md</code></a> 保留作历史参照。
离线可用(reveal.js 已 vendor 进仓库);代码链接需通过本地 HTTP 服务打开才能正常跳转
<code>python3 -m http.server 8000</code>,见大纲「材料与播放方式」)。</p>
<ul>
<li><a class="day" href="day1.html">Day 1 · 开训 + C++ 回顾/工具链</a>
<div class="desc">团队建设/Scrum/C++企业应用(讨论,无编码) → C++精选回顾 + CMake实操 + Vcpkg概念</div></li>
<li><a class="day" href="day2.html">Day 2 · Qt 开发环境/信号槽/基础控件/布局与 QSS</a>
<div class="desc">p03/ch0204 项目结构/信号槽 → p03/ch08 控件/布局 + QSS新示例 → 计算器案例</div></li>
<li><a class="day" href="day3.html">Day 3 · 高级控件 Model/View / 事件机制 / 文件IO与JSON</a>
<div class="desc">Model/View新示例 + p03/ch09 事件拦截 → p03/ch11 文件IO + JSON新示例 → 本地数据存储案例</div></li>
<li><a class="day" href="day4.html">Day 4 · 2D绘图/多线程/动画框架与状态机</a>
<div class="desc">p03/ch10 2D绘图 + QThread新示例 → 动画+状态机新示例 → 交互式小游戏案例</div></li>
<li><a class="day" href="day5.html">Day 5 · 团队协同开发/综合案例复盘/OpenCV可选</a>
<div class="desc">团队协同开发(概念,用方案书图) → 综合案例复盘 + OpenCV/AI自学指针</div></li>
<li><a class="day" href="day1.html">Day 1 · 开训 + C++ 回顾与提升(全天 C++</a>
<div class="desc">技术版图/环境/CMake/OOP 回顾(含 PRETEST 摸底) → 继承多态虚函数/内存管理与★所有权/STL → 现代 C++ 钩子</div></li>
<li><a class="day" href="day2.html">Day 2 · Qt 概述/创建项目/第一个程序 + 信号槽 → 计算器</a>
<div class="desc">p03/ch0204 + ★栈控件双重释放/★lambda 第五参数/★重复 connect/★中文编码 → 计算器案例</div></li>
<li><a class="day" href="day3.html">Day 3 · QMainWindow/QDialog + 布局/常用控件/QSS</a>
<div class="desc">p03/ch05–06 主窗口与对话框(☆模态非模态生命周期) → p03/ch08 布局控件 + QSS 美化 → 计算器升级</div></li>
<li><a class="day" href="day4.html">Day 4 · 事件机制/绘图/表格 + 文件系统</a>
<div class="desc">p03/ch0910 事件与绘图 + QTableWidget → p03/ch11 文件 IO + JSON + ★工作目录/★文件编码 → 数据存储小工具</div></li>
<li><a class="day" href="day5.html">Day 5 · 综合项目案例讲解:学员信息管理器(全天)</a>
<div class="desc">Day14 收束 + 提升:QThread 防卡界面 + Git/考核规范 + 分组定方向 → Day6–10 项目开发</div></li>
</ul>
</body>
</html>