From 0f81edcc740c0d7411112eaab4eaae17b2c7c71b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=97=E5=B9=B3?= Date: Sun, 5 Jul 2026 12:49:44 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B9=BB=E7=81=AF=E7=89=87=E6=8C=89=204=20?= =?UTF-8?q?=E5=A4=A9=E7=89=88=E9=87=8D=E6=8E=92=EF=BC=9ADay1=20=E5=85=A8?= =?UTF-8?q?=20C++=E3=80=81Day3=20=E5=90=AF=E7=94=A8=20ch05/ch06=E3=80=81Da?= =?UTF-8?q?y5=20=E7=BB=BC=E5=90=88=E6=A1=88=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 失效 --- docs/teaching/slides/day1.html | 506 ++++++++++++++------------------ docs/teaching/slides/day2.html | 484 +++++++++++++----------------- docs/teaching/slides/day3.html | 334 ++++++++------------- docs/teaching/slides/day4.html | 335 ++++++++++----------- docs/teaching/slides/day5.html | 316 ++++++++++---------- docs/teaching/slides/index.html | 37 +-- 6 files changed, 880 insertions(+), 1132 deletions(-) diff --git a/docs/teaching/slides/day1.html b/docs/teaching/slides/day1.html index c9156ae..819007e 100644 --- a/docs/teaching/slides/day1.html +++ b/docs/teaching/slides/day1.html @@ -2,7 +2,7 @@ -Day 1 · 开训 + C++ 回顾/工具链 +Day 1 · 开训 + C++ 回顾与提升(全天) @@ -15,277 +15,55 @@

Day 1

-

开训 + C++ 回顾/工具链

-

C++/Qt 方向实训 · 第 1 / 5 天 · 对齐官方方案书 Day1

+

开训 + C++ 回顾与提升(全天 C++)

+

C++/Qt 方向实训 · 第 1 / 4 个讲授日 · 对齐 2026-07 新版 10 天安排

-

本教案假定学员已掌握 C++ 基础语法与 OOP 概念——方案书对今天下午的 - 官方定位是"回顾和提升",不是零基础教学。今天开始的 5 天覆盖方案书 C++方向 - Day1–5 全部课堂内容,Day6–10 是纯项目实作。

+

本教案假定学员已掌握 C++ 基础语法与 OOP 概念——今天定位是"回顾和提升", + 不是零基础教学。Day1–4 讲授,Day5 综合案例,Day6–10 项目开发与答辩。 + 大纲:docs/teaching/OUTLINE_4DAY.md

-

模块 B 开始前:10 分钟摸底测验

-

这条假设不是拍脑袋定的——建议模块 B 讲授前先用 - docs/teaching/PRETEST.md - (10 题、10 分钟)实测一次,按结果决定原节奏推进还是回退到语法讲解。

+

OOP 回顾开讲前:10 分钟摸底测验

+

受众假设要实测——用 docs/teaching/PRETEST.md + (10 题、10 分钟、A/B 平行卷)测一次,按结果决定回顾环节推进速度(分层预案见该文件)。

- +
-

模块 A · 讲授 35min

-

团队建设 / 敏捷 Scrum / C++ 企业应用

+

上午 · 讲授 08:30–11:00(讲 25–30min + 随堂练 10–15min 循环)

+

开训 / C++ 技术版图 / 环境与 CMake / OOP 回顾

    -
  • 团队建设与文化、实训安排与考评要求
  • -
  • 项目开发过程管理全景(JIRA / Git / OASIS / Wiki)
  • -
  • 敏捷 Scrum 项目开发管理
  • -
  • C++ 在企业中的应用
  • +
  • 开训典礼:团队文化 · 实训安排 · 考评要求(组织方主持)
  • +
  • C++ 常用技术和框架(讲座)
  • +
  • 开发环境搭建 + Makefile/CMake 项目构建
  • +
  • PRETEST 摸底(10min)→ 面向对象基础回顾(类/对象/封装)
-

实习实训服务流程

- -

出处:docs/TRAINING_PLAN_2026.md 「六、实习实训服务流程」

-
- -
-

服务流程:核心概念

+

C++ 常用技术和框架(讲座)

-

核心概念

+

核心概念——C++ 的四大应用版图

    -
  • 需求分析→需求评审→项目研发/代码实现→阶段评审→项目测试/交付——每个环节都有明确的 - "评审"节点把关,不是写完代码就结束,这就是接下来 Day6–10 项目实作要走的完整流程。
  • +
  • 桌面客户端:Qt(本次实训主线)、跨平台 GUI;
  • +
  • 服务端/基础设施:高性能网络服务、数据库引擎、游戏服务器;
  • +
  • 嵌入式/车载:资源受限环境里 C++ 仍是第一梯队选择;
  • +
  • 游戏/图形:UE 引擎、渲染管线。
-
-

常见误区

-
    -
  • 把"需求分析"当成走过场:跳过评审直接开始编码,后期返工成本远高于前期多花时间对齐需求。
  • -
-
-
- -
-

质量保证:四平台一标准

- -

出处:docs/TRAINING_PLAN_2026.md 「七、质量保证」

-
- -
-

四平台一标准:核心概念

-

核心概念

+

而且它还在进化

    -
  • JIRA(任务跟踪)+ Git(版本管理)+ Wiki 知识库 + OASIS 绿洲实训管理平台,四个工具 - 分别对应"谁在做什么/代码怎么变化/知识怎么沉淀/进度怎么被看到"四个不同维度,缺一个都会 - 让协作出现盲区。
  • +
  • C++11 起三年一版:11「几乎新语言」→ 17 实用大件 → 20 四大件(Concepts/Ranges/协程/模块)。 + 今天下午末尾有专门一页巡礼;深入自学: + MODERN_CPP.md · + MODERN_CPP_20PLUS.md
-
-

常见误区

-
    -
  • 只用 Git 不用 Jira:代码有了版本历史,但"谁负责什么、进度到哪"没有可见记录, - 团队协作时容易重复劳动或漏项。
  • -
-
-
-
- - -
-
-

模块 A · 讨论 95min(分组,非编码)

-

动手:分组讨论

-

今天上午没有编码练习——先把协作流程和项目方向定下来,Day6–10 - 的项目实作会真的用到今天讨论的结论。

-
-
-
-

任务卡 1 · 协作分工

-
-
目标
-
结合服务流程图,说清楚本组打算怎么分工使用 Jira(任务跟踪)+ Git(代码版本管理)
-
验收标准
-
每组提交一句话分工说明(谁负责建 Jira 看板 / 谁负责 Git 仓库初始化),能在图上指出对应环节
-
基础知识 · 参考
-
上一节"实习实训服务流程"图 · - TRAINING_PLAN_2026.md 「七、质量保证 (一)(二)」
-
-
-
-
-
-

任务卡 2 · 选定项目方向

-
-
目标
-
选定实训期间打算做的项目方向(Linux C 方向,或 C++/Qt 方向的备选商业项目之一)
-
验收标准
-
每组报出项目名称 + 一句话说明选择理由
-
基础知识 · 参考
-
TRAINING_PLAN_2026.md 「八、备选商业项目介绍」
-
-
-
-
- - -
-
-

模块 B · 讲授 35min(巡礼式,压缩讲解)

-

C++ 回顾 / CMake / Vcpkg

-

只挑「学过但容易讲不透」的点,不逐章展开——方案书官方定位是 - "回顾和提升",深度让位于 Day2–5 的 Qt 内容。

-
- -
-

namespace 作用域

-
-

核心概念

-
    -
  • "作用域"不是"文件顺序"——很多人误以为 namespace 只是给名字加前缀,实际上它定义的是 - 一段可以被跨文件延续、跨块合并的作用域,同一个命名空间可以在多个文件里反复打开、追加内容。
  • -
-
-
-

常见的坑

-
    -
  • 在头文件里写 using namespace std;:会把整个命名空间污染带给所有 - include 这个头文件的地方,容易引发命名冲突,规范做法是只在 .cpp 里用,或干脆不用、 - 全程写限定名。
  • -
-
-

出处:p01/ch03/namespace_syntax.cpp, - 对照 docs/ERRATA.md

-
- -
-

函数重载决议

-
-

核心概念

-
    -
  • 编译器靠"名字改写"(name mangling)把重载函数变成不同的符号;二义性往往不是代码写 - 错了,而是重载集合里存在两个"一样好"的候选,编译器无法替你选。
  • -
-
-
-

常见的坑

-
    -
  • 隐式类型转换会扩大重载候选集——比如 int/double 都能转 - float 参数,传一个 long 进去可能触发意料之外的重载版本。
  • -
-
-

出处:p01/ch03/function_overload_mechanism.cpp

-
- -
-

引用的本质

-
-

核心概念

-
    -
  • 把引用理解成"自动解引用的 const 指针"——这个实现视角能解释引用为什么必须初始化、 - 为什么不能重新绑定到另一个对象。
  • -
-
-
-

常见的坑

-
    -
  • 返回局部变量的引用:函数结束局部变量已销毁,返回的引用变成悬空引用,后续访问是 - 未定义行为——和返回局部变量指针是同一类坑。
  • -
-
-

出处:p01/ch03/reference_essence.cpp

-
- -
-

虚析构与内存泄漏

-

-Base* p = new Derived();
-delete p;   // 如果 ~Base() 不是 virtual,~Derived() 不会被调用——经典泄漏
-    
-
-

核心概念

-
    -
  • 通过基类指针 delete 派生类对象时,只有基类析构函数是 virtual - 才会走虚函数表找到派生类的析构函数——这是"多态"在析构这个场景下的具体体现。
  • -
-
-
-

常见的坑

-
    -
  • 只要一个类可能被继承、且可能通过基类指针删除派生对象,就应该把析构函数声明为 - virtual——即使当前基类本身没什么要清理的,这是个防御性约定。
  • -
-
-

出处:p01/ch04/s08/virtual_dtor_purpose.cpp

-
- -
-

模板与 STL 速览

-
-

核心概念

-
    -
  • 只点名,不展开:泛型编程(函数模板/类模板)让同一份代码适配不同类型,容器/迭代器 - 是 Day6–10 项目实作里会大量用到的基础设施——迭代器是"容器和算法之间的统一接口",这也是 - STL 能让算法脱离具体容器类型复用的关键。
  • -
-
-
-

常见的坑

-
    -
  • 容器扩容/删除元素后,之前保存的迭代器可能失效(如 vector 扩容), - 继续用失效的迭代器是未定义行为——遍历中增删元素前要先弄清对应容器的迭代器失效规则。
  • -
-
-

出处:p01/ch05/class_template_basics.cpp、 - p01/ch09/s03/vector_iterator.cpp

-
- -
-

C++11/14/17/20+:一门仍在进化的语言

-
-

核心概念——每 3 年一次实打实的能力升级,不是小修小补

-
    -
  • C++11("现代 C++ 元年"):auto 类型推导、智能指针 - unique_ptr/shared_ptr(RAII 终结手动 new/delete)、 - 右值引用 && + 移动语义(消除不必要的拷贝)、lambda 表达式、 - 语言级并发 std::thread/mutex/atomic、 - nullptrconstexpr
  • -
  • C++14:泛型 lambda(参数写 auto)、函数返回类型自动推导、 - std::make_unique 补齐。
  • -
  • C++17:结构化绑定 auto [a,b] = pairif/switch - 带初始化语句、std::optional/variant/string_view、 - 标准库终于有跨平台的 std::filesystem
  • -
  • C++20(号称"自 C++11 以来最大的一次变革"):concepts 概念(模板约束,报错从 - 几百行天书变成一句话)、ranges 范围库(view | filter | transform 管道式组合)、 - coroutines 协程(co_await 把异步代码写成同步的样子)、modules 模块(挑战头文件 - 体系)、<=> 三路比较一行换 6 个运算符。
  • -
-
-
-

常见的坑

-
    -
  • 新标准 ≠ 现在能用:生产环境的编译器/CI 常年落后最新标准 1–2 个版本,用新特性前先确认 - 目标工具链 -std= 支持到哪。本仓库出于教学稳定性选 C++17 作为基线(顶层 - CMakeLists.txt),可用 tools/std_probe.py 自己验证某特性最早从哪个 - 标准开始能编译(宿主 gcc 13.3 原生支持到 C++23)。
  • -
-
-
-

缺口说明

-

本节为纯概念巡礼,无对应仓库示例——具体新特性的动手练习超出本次"C++回顾"压缩讲解范畴, - 感兴趣可用 tools/std_probe.py + tools/demo_versions/ 自行探索 - (参见 docs/VERSION_NOTES.md)。

-
@@ -296,51 +74,72 @@ cmake --build build # 144 个目标全绿

核心概念

  • CMake 是"生成构建系统的工具"而不是构建系统本身:cmake -S -B 读 - CMakeLists.txt 生成 Ninja/Makefile 工程,cmake --build 才是 - 真正编译。option(BUILD_QT_PART) 控制是否编译 Qt 部分,每个示例一个 - add_executable 目标,这就是"一文件一目标"约定的由来。
  • + CMakeLists.txt 生成 Ninja/Makefile 工程(Makefile 是它的生成目标之一—— + 这就是两者的关系),cmake --build 才是真正编译。 + option(BUILD_QT_PART) 控制是否编译 Qt 部分,每个示例一个 + add_executable 目标。

常见的坑

    -
  • 改了 CMakeLists.txt 却没重新 cmake 配置(只 build): - 大多数改动 CMake 会自动侦测重新配置,但换了生成器/清了缓存等情况需要重新走 - -S -B 一次。
  • +
  • 改了 CMakeLists.txt 却没重新配置:大多数改动 CMake 会自动侦测, + 但换生成器/清缓存后需要重新走一次 -S -B
-

出处:仓库根 CMakeLists.txt

+

出处:仓库根 CMakeLists.txt + · 包管理器一分钟:vcpkg 用 toolchain 文件让 find_package 自动找到三方库; + 本仓库选"隔离 Qt 手动装",两种思路都要认识。

-

Vcpkg 包管理器(概念,无演示)

+

★ 编译错误 vs 链接错误:排查方向完全不同

+
main.cpp:12:5: error: 'foo' was not declared     ← 编译期:带文件行号
+main.o: undefined reference to `Widget::bar()'   ← 链接期:没有行号

核心概念

    -
  • C++ 生态的包管理器,解决"第三方库怎么装、怎么被 CMake 找到"的问题——一次 - vcpkg install,配合 CMake 的 toolchain 文件,find_package - 就能自动找到头文件和库。
  • +
  • error: 带行号 = 编译期问题(语法/类型/没声明)——回代码里查那一行; + undefined reference 无行号 = 链接期问题(有声明没定义/缺库/缺目标文件)—— + 查"定义在哪、链没链进来",回头检查语法是南辕北辙。

常见的坑

    -
  • 不是所有第三方库都必须用包管理器装——本仓库用的是"隔离 Qt 手动装" - (tools/install_qt_host.sh)而非 vcpkg,两种思路都要认识:包管理器省心但 - 引入额外工具链,手动隔离装则精确控制版本/路径,各有取舍。
  • +
  • 中初级学员见到 undefined reference 的第一反应是检查拼写和语法——方向就错了。 + 明天 Qt 的"忘写 Q_OBJECT"报的正是链接错误,且报错完全不提宏,记住今天这页。
-
-

缺口说明

-

本仓库暂无 vcpkg 可运行示例——安装真实 vcpkg 工具链超出最小示例范畴,此处仅作概念介绍。

+
+ +
+

OOP 回顾:类 / 对象 / 封装 + namespace

+
+

核心概念

+
    +
  • 封装 = 数据和操作绑定 + 访问控制(public/private 是给"人"划的边界,不是安全机制); + 构造/析构对应对象生命周期的两端——这个心智模型下午的"所有权"还要用。
  • +
  • namespace 的"作用域"不是"文件顺序":可跨文件延续、跨块合并。
  • +
+
+

常见的坑

+
    +
  • 头文件里 using namespace std; 会把命名空间污染带给所有 include 它的 + 地方——只在 .cpp 用,或全程写限定名。
  • +
+
+

出处:p01/ch04/(类与对象各节)、 + p01/ch03/namespace_syntax.cpp, + 对照 docs/ERRATA.md

- +
-

模块 B · 练习 95min

+

上午 · 课堂练习 11:00–12:00(工程师指导、答疑)

动手:走一遍真实构建流程

@@ -358,13 +157,165 @@ cmake --build build # 144 个目标全绿
+
+ + +
+
+

下午 · 讲授 13:30–16:00

+

继承与多态 / 内存管理 / STL

+
    +
  • C++ 核心特性:继承与多态、虚函数(含虚析构)
  • +
  • 内存管理:指针、引用与动态分配(含★所有权模型)
  • +
  • STL 常用容器简介
  • +
  • 收尾:现代 C++ 巡礼(兴趣钩子)
  • +
+
+ +
+

函数重载决议

+
+

核心概念

+
    +
  • 编译器靠"名字改写"(name mangling)把重载函数变成不同符号;二义性往往不是代码写错, + 而是重载集合里有两个"一样好"的候选,编译器无法替你选。重载(编译期,同类内) vs + 重写(运行期,继承体系) 是两码事。
  • +
+
+
+

常见的坑

+
    +
  • 隐式类型转换会扩大重载候选集——传一个 long 进去可能触发意料之外的重载版本。
  • +
+
+

出处:p01/ch03/function_overload_mechanism.cpp

+
+ +
+

虚函数与虚析构

+

+Base* p = new Derived();
+delete p;   // 如果 ~Base() 不是 virtual,~Derived() 不会被调用——经典泄漏
+    
+
+

核心概念

+
    +
  • 通过基类指针 delete 派生类对象时,只有基类析构是 virtual + 才会走虚函数表找到派生类析构——"多态"在析构场景下的具体体现。
  • +
+
+
+

常见的坑

+
    +
  • 只要类可能被继承 + 可能经基类指针删除,析构就该 virtual——防御性约定。 + (明天会看到 Qt 的 QObject 体系全线虚析构。)
  • +
+
+

出处:p01/ch04/s08/virtual_dtor_purpose.cpp

+
+ +
+

引用的本质

+
+

核心概念

+
    +
  • 把引用理解成"自动解引用的 const 指针"——这个实现视角能解释为什么必须初始化、 + 为什么不能重新绑定。
  • +
+
+
+

常见的坑

+
    +
  • 返回局部变量的引用:函数结束局部变量已销毁,引用悬空,访问是未定义行为—— + 和返回局部变量指针同一类坑。
  • +
+
+

出处:p01/ch03/reference_essence.cpp

+
+ +
+

★ 所有权:谁负责 delete?

+
+

核心概念

+
    +
  • 内存管理的核心问题不是"会不会 new",而是"这块资源归谁管、由谁释放"。 + 现代答案是 RAII/智能指针:unique_ptr 独占所有权、shared_ptr + 共享所有权(详见 MODERN_CPP.md §2)。 + 一个对象只能有一个所有者。
  • +
+
+
+

常见的坑

+
    +
  • 双重释放、忘记释放、释放后使用——三兄弟全部源于所有权不明。
  • +
  • 明天的伏笔:Qt 用"父子树"回答同一个问题(parent 析构自动 delete 子对象)。 + 两套所有权体系不能混用——交给 Qt 树管的对象,不要再用智能指针/栈/手动 delete 管。
  • +
+
+
+ +
+

模板与 STL 常用容器

+
+

核心概念

+
    +
  • 泛型编程让同一份代码适配不同类型;vector/map/string + 是项目实作三件套;迭代器是"容器和算法之间的统一接口"——STL 算法能脱离具体容器复用的关键。
  • +
+
+
+

常见的坑

+
    +
  • 容器扩容/删除元素后,旧迭代器可能失效(如 vector 扩容搬家),继续用是 + 未定义行为——遍历中增删元素前先弄清该容器的失效规则。
  • +
+
+

出处:p01/ch05/class_template_basics.cpp、 + p01/ch09/s03/vector_iterator.cpp

+
+ +
+

钩子 · C++11/14/17/20+:一门仍在进化的语言

+
+

核心概念——每 3 年一次实打实的能力升级

+
    +
  • C++11:auto · 智能指针 · 移动语义 · lambda · 语言级并发 —— "现代 C++ 元年";
  • +
  • C++14:泛型 lambda · 返回类型推导 · make_unique
  • +
  • C++17:结构化绑定 · if-init · optional/variant/string_view · filesystem(本仓库基线);
  • +
  • C++20:Concepts · Ranges · 协程 · 模块 —— "自 C++11 以来最大变革"。
  • +
+
+
+

常见的坑

+
    +
  • 新标准 ≠ 现在能用:生产工具链常年落后 1–2 版。用 tools/std_probe.py + 自己验证某特性从哪个标准开始能编译(宿主 gcc 13.3 支持到 C++23)。
  • +
+
+

自学入口:MODERN_CPP.md(11/14/17, + 示例全部可在本仓库基线下编译) · + MODERN_CPP_20PLUS.md(20/23/26 前瞻, + 含本机 GCC 实测矩阵)

+
+
+ + +
+
+

下午 · 课堂作业 16:00–17:00(工程师答疑)

+

作业

+

任务卡 2 · 修一个真实的坑

目标
docs/ERRATA.md - 挑 2 条记录,理解"wiki 原文错在哪、现在的写法为什么对"
+ 挑 2 条与今天知识点相关的记录(虚析构/引用/重载类),理解"wiki 原文错在哪、为什么"
验收标准
能用一句话向同组同学解释清楚,不能只念代码
基础知识 · 参考
@@ -377,13 +328,12 @@ cmake --build build # 144 个目标全绿

任务卡 3(选做)· 版本探针

目标
-
tools/std_probe.py 验证一个标准版本差异点(如三目运算符可否作左值)
+
tools/std_probe.py 验证一个标准版本差异点
验收标准
-
跑出至少一个"该标准报错、另一个标准不报错"的对比结果,对照 +
跑出至少一个"该标准报错、另一个标准不报错"的对比,对照 docs/VERSION_NOTES.md §2
基础知识 · 参考
-
tools/std_probe.py · - docs/VERSION_NOTES.md
+
tools/std_probe.py
@@ -395,10 +345,10 @@ cmake --build build # 144 个目标全绿

验收

cmake --build build --target all   # Part1,144 个目标全绿
-

每组提交一句话分工说明 + 至少 1 条 ERRATA 坑点讲解。

+

每人完成构建全绿 + 2 条 ERRATA 坑点讲解;PRETEST 结果已归档(决定后续调速)。

- +

返回目录 · 下一天 →

diff --git a/docs/teaching/slides/day2.html b/docs/teaching/slides/day2.html index 2ade562..99ed860 100644 --- a/docs/teaching/slides/day2.html +++ b/docs/teaching/slides/day2.html @@ -2,7 +2,7 @@ -Day 2 · Qt 开发环境/信号槽/基础控件/布局与 QSS +Day 2 · Qt 入门 / 信号槽 / 计算器案例 @@ -15,11 +15,11 @@

Day 2

-

Qt 开发环境 / 信号槽 / 基础控件 / 布局与 QSS

-

C++/Qt 方向实训 · 第 2 / 5 天 · 讲:练 ≈ 1:2.7

+

Qt 概述 / 创建项目 / 第一个程序 / 信号槽 → 计算器

+

C++/Qt 方向实训 · 第 2 / 4 个讲授日

开始前准备(一次性)

@@ -30,355 +30,270 @@ cmake --build build
- +
-

模块 A · 讲授 35min

-

项目结构 / 信号槽

-
    -
  • 最小 Qt 程序结构
  • -
  • 控件创建与显示
  • -
  • 内置 / 自定义 / lambda 信号槽
  • -
+

上午 · 讲授 08:30–11:00

+

Qt 概述 / 创建项目 / 第一个程序

-

最小 Qt 程序:核心概念

+

Qt 概述

核心概念

    -
  • QApplication 是整个进程唯一的"事件循环拥有者"——app.exec() - 阻塞在这里进入事件循环,不断从操作系统取事件(鼠标/键盘/定时器/重绘请求)分发出去, - 程序的"活着"就是这个循环在转,不是运行完就退出的普通程序。
  • -
  • Q_OBJECT 宏触发 moc(meta-object compiler)在编译前生成额外代码—— - 信号槽、属性系统、qobject_cast 等运行时反射能力都靠这份生成代码实现, - 这是 Qt 自己的预处理步骤,不是普通 C++ 语法。
  • +
  • Qt = 跨平台 C++ 应用框架:一份代码,Windows/Linux/macOS 原生编译;模块化组织—— + 本课程用 Core(对象/容器/IO)、GuiWidgets(桌面控件)。
  • +
  • 版本纪律:本实训统一用隔离安装的 Qt 5.14.2.qt514/),不碰系统 + Qt——顶层 CMakeLists 有版本断言,连错版本直接 FATAL_ERROR。这是工程上 + "锁依赖版本"的真实示范。
  • +
+
+

出处:仓库根 CMakeLists.txt(版本断言一节)

+
+ +
+

创建 Qt 项目(CMake 方式)

+
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)
+
+

核心概念

+
    +
  • 昨天的 find_package 概念今天落地:CMake 按 CMAKE_PREFIX_PATH + 找到 Qt;AUTOMOC 自动跑 moc(元对象编译器)——它为 Q_OBJECT + 类生成信号槽所需的代码,下一页就见到。
  • +
+
+

出处:p03/CMakeLists.txt、 + p03/ch02/CMakeLists.txt

+
+ +
+

第一个 Qt 小程序

+
+

核心概念

+
    +
  • QApplication 是唯一的事件循环拥有者,app.exec() 阻塞进入 + 事件循环分发系统事件;Q_OBJECT 宏触发 moc 生成信号槽/属性系统所需的元对象代码。

常见的坑

    -
  • 一个进程只能有一个 QApplication 实例,创建第二个直接崩溃/未定义行为。
  • -
  • 忘记调用 exec():窗口一闪而过就退出,因为根本没进入事件循环。
  • -
  • 类里用了信号槽却忘记写 Q_OBJECT:编译能过,链接阶段报 - undefined reference——报错信息完全不提示"缺宏",是新手最常踩的坑。
  • +
  • 一个进程只能有一个 QApplication;忘记 exec() 窗口一闪而过;
  • +
  • 用了信号槽却忘 Q_OBJECT:报 undefined reference—— + 链接错误(昨天那页!),报错完全不提"缺宏",知道这一条能省一小时。
+

出处:p03/ch02/minimal_qt_app.cpp

-

最小 Qt 程序:QApplication + exec()

-

-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();              // 进入事件循环,程序在这里“活着”
-}
-    
-

出处:p03/ch02/minimal_qt_app.cpp(注意仓库版本额外注入了 - QTimer::singleShot 200ms 自动退出,便于离屏自动化验证,真实项目不需要这一行)

-
- -
-

控件创建:对象树与父子所有权

+

控件创建与对象树

核心概念

    -
  • QObject 有一套"对象树"父子所有权模型:给控件传入 parent 后, - parent 被销毁时会自动 delete 所有子对象——这就是为什么 Qt 代码里到处是 - new 却几乎不见 delete
  • -
  • 两种写法(先构造后设属性 / 构造时一次性传参)本质相同,父对象的作用一样。
  • +
  • QObject 对象树 = Qt 的所有权答案(接昨天"谁负责 delete"):创建控件把 parent + 传进去,parent 析构自动 delete 所有子对象——整棵树只需要管好根。

常见的坑

    -
  • 忘记传 parent:控件永远不会自动释放(真实内存泄漏),也不会自动出现在 - 窗口里(没有父容器就没有布局位置)。
  • -
  • move() 手动摆放坐标的控件,一旦外层套上布局管理器,手动坐标会被 - 布局在下次刷新时覆盖——这是"混用绝对定位和布局"最常见的诡异 bug 来源。
  • +
  • 忘记传 parent:对象没人管,真实内存泄漏;
  • +
  • move() 手动定位的控件一旦套上布局会被布局接管覆盖。
+

出处:p03/ch03/button_creation.cpp

-

控件创建:两种写法

+

★ 栈对象与父子所有权打架


-// 第一种:先构造再设属性
-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(栈地址!)→ 崩溃
     
-

出处:p03/ch03/button_creation.cpp

-
- -
-

信号槽:观察者模式 + moc

核心概念

    -
  • 信号槽本质是"观察者模式"的 Qt 实现:发送者不知道、也不关心谁在监听,彻底解耦发送方 - 和接收方——这和"直接调用某个函数"是完全不同的设计取向。
  • -
  • connect() 背后依赖 moc 生成的元对象信息做类型检查:新语法 - &Class::signal编译期检查,字符串旧语法 - SIGNAL()/SLOT()运行时检查,容易悄悄连接失败。
  • -
  • 连接方式分 Direct(同线程直接调用)/ Queued(跨线程排队,下次事件循环处理)/ - Auto(默认,同线程 Direct、跨线程自动 Queued)——Day4 讲 QThread 时还会用到。
  • +
  • 父子树会 delete 子对象 ⇒ 交给父子树管的对象必须建在堆上。一个对象只能有 + 一个所有者:给了 parent,就不能再由栈/智能指针/手动 delete 管。

常见的坑

    -
  • 旧的字符串语法 SIGNAL(clicked()) 函数名/参数类型写错不会编译报错, - 只会运行时"连接失败"且经常没有提示——能用新语法就不用旧语法,编译期就能发现问题。
  • -
  • 重载信号(如右侧例子的两个 hungry())不加 QOverload - 消歧义,编译器不知道该连哪个重载,直接报错。
  • +
  • 声明顺序反过来时这段代码"恰好不崩"——不要赌析构顺序的运气,规则只有一条。 + 现场演示:tools/run_qt.sh qt_pitfall_stack_widget --crash(真崩给你看)。
-
- -
-

信号槽:内置 / 自定义 / 重载消歧义

-

-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<>::of(&Teacher::hungry), s, QOverload<>::of(&Student::treat));
-connect(t, QOverload<QString>::of(&Teacher::hungry), s, QOverload<QString>::of(&Student::treat));
-    
-

出处:p03/ch04/custom_signal_slot.cpp; - lambda 槽 + 值捕获/引用捕获对比见 - lambda_signal_slot.cpp

+

复现示例:examples/pitfall_stack_widget/ + (README

- +
-

模块 A · 练习 95min

-

动手:第一个 Qt 程序

+

上午 · 课堂练习 11:00–12:00(工程师指导、答疑)

+

动手:跑通最小程序

-

任务卡 1 · 跑通最小程序

+

任务卡 1 · 跑通并改造最小程序

目标
-
编译运行 minimal_qt_appbutton_creation
+
编译运行 minimal_qt_appbutton_creation;把窗口标题和 + 按钮文字改成自己的学号+姓名(中文——如果乱码了,记住现象,下午有一页专讲)
验收标准
-
QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app 与 - button_creation 均退出码为 0,无崩溃
+
QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app 退出码 0; + 有 X 环境下能看到自己改的中文标题正常显示
基础知识 · 参考
p03/ch02/minimal_qt_app.cpp · p03/ch03/button_creation.cpp
+
+ + +
-
-

任务卡 2 · 自定义信号槽改文本

-
-
目标
-
给一个按钮连接自定义信号槽:点击后改变一个 QLabel 的文本内容
-
验收标准
-
编译通过;离屏或有 X 环境下点击/模拟点击后 QLabel 文本确实改变; - 用 connect 语法而非直接函数调用
-
基础知识 · 参考
-
custom_signal_slot.cpp(主参考)· - lambda_signal_slot.cpp(lambda 槽写法对比)
-
+

下午 · 讲授 13:30–16:00

+

信号槽机制 → 计算器案例

+
+ +
+

信号槽:内置 / 自定义 / lambda

+
+

核心概念

+
    +
  • 观察者模式的 Qt 实现:发送者只 emit,不知道谁在听——对象间彻底解耦;
  • +
  • 新语法 &Class::signal 编译期类型检查;旧字符串语法 + SIGNAL()/SLOT() 只在运行时检查;连接方式 Direct/Queued/Auto(Day5 跨线程时回收)。
  • +
+
+
+

常见的坑

+
    +
  • 旧语法写错不报编译错误,只是运行时静默连接失败;
  • +
  • 重载信号不加 QOverload 消歧义直接编译报错。
  • +
+
+

出处:p03/ch04/builtin_signal_slot.cpp · + custom_signal_slot.cpp · + lambda_signal_slot.cpp

+
+ +
+

★ lambda 槽的生命周期与第五参数

+

+connect(sender, &Sender::sig, [obj]{ obj->f(); });        // ✗ obj 销毁后仍触发
+connect(sender, &Sender::sig, obj, [obj]{ obj->f(); });   // ✓ 第五参数=context
+    
+
+

核心概念

+
    +
  • 第五参数(context object)决定连接跟谁的生命周期挂钩:传了 context,对象析构时 + Qt 自动断开连接。规则:lambda 捕获了某个 QObject,就把它作为第五参数传进去
  • +
+
+
+

常见的坑

+
    +
  • 不传 context:接收方销毁后 lambda 仍被调用,捕获的指针就是悬空指针——UB 可能崩、 + 也可能"打印出看似正常的旧值",后者更危险(开发机正常、演示现场炸)。
  • +
+
+

复现示例:examples/pitfall_lambda_capture/ + (README, + --dangle 看真实悬空访问)

+
+ +
+

★ 重复 connect:点一次按钮弹两次框

+
+

核心概念

+
    +
  • connect 每调一次新增一条连接,Qt 不去重——同一对信号槽连两次, + 槽就被调两次。修复:connect 收拢到构造函数只做一次(首选);或 + Qt::UniqueConnection(只对成员函数指针有效,lambda 防不了)。
  • +
+
+
+

常见的坑

+
    +
  • connect 写在会被多次执行的路径上(每次打开对话框都跑一遍的"初始化"函数)—— + 现象是"点一次弹两次",第一反应查按钮,根源在连接次数。
  • +
+
+

复现示例:examples/pitfall_double_connect/ + (README

+
+ +
+

★ QString 与中文编码

+
+

核心概念

+
    +
  • QString 内部是 UTF-16,和 std::string(字节串)是两种东西, + 互转必须指明编码:toUtf8()/fromUtf8();中文字面量建议 + QStringLiteral("中文") + 源码文件保存为 UTF-8。
  • +
+
+
+

常见的坑

+
    +
  • 源码不是 UTF-8、或用 QString(char*) 隐式转换中文字面量 → 界面乱码。 + 上午改学号姓名乱码的同学:根源就在这页。Day4 读写文件时编码问题还会再来一次。
  • +
+
-
-

任务卡 3(选做)· 重载信号消歧义

-
-
目标
-
仿 builtin_signal_slot.cpp,给一个类定义两个同名不同参的信号,用 - QOverload 消歧义连接
-
验收标准
-
两个重载信号都能各自触发对应的槽
-
基础知识 · 参考
-
p03/ch04/builtin_signal_slot.cpp
-
+

案例 · 简易计算器(布局脚手架:先用后学)

+

+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); // 行、列坐标摆键盘
+    
+
+

组装思路

+
    +
  • 数字/运算符按钮(上午的 button_creation)+ 点击信号槽(刚学的三种写法) + + QGridLayout 脚手架排键盘 + QLabel/QLineEdit 显示结果。
  • +
  • 项目 = 已学知识点的组装——这是 Day5 综合案例和 Day6–10 项目的预演。
  • +
- +
-

模块 B · 讲授 35min

-

常用控件 / 布局管理 / QSS 美化

-
- -
-

资源系统:.qrc 是什么

-
-

核心概念

-
    -
  • :/Image/boat.jpg 这种路径不是文件系统路径,是 Qt 资源系统(QRC)—— - 图片等资源在编译期被打包进可执行文件本身,运行时不依赖外部文件是否存在,这对 - "打包发布"很关键。
  • -
-
-
-

常见的坑

-
    -
  • .qrc 里资源路径/文件名和代码里 :/... 引用的不一致,会在 - 运行时静默失败(QPixmap 为空、图不显示),不会报编译错误。
  • -
  • 忘记把 .qrc 接入 CMake 的 qt5_add_resources/AUTORCC, - 资源系统压根不生效。
  • -
-
-
- -
-

常用控件:QLabel 显示图片

-

-QPixmap pixmap;
-pixmap.load(":/Image/boat.jpg");   // 从 Qt 资源系统(.qrc)加载
-QLabel* label = new QLabel(this);
-label->setPixmap(pixmap);
-    
-

出处:p03/ch08/label_pixmap.cpp; - HTML 富文本标签见 label_text_html.cpp

-
- -
-

布局管理:为什么不手算坐标

-
-

核心概念

-
    -
  • 布局管理器(QLayout)接管子控件的位置和大小计算,随窗口缩放自动重新 - 排布——这是"不要自己算坐标"的根本原因,也是跨平台/跨分辨率 UI 能正确显示的基础。
  • -
  • 布局对象本身也遵循 QObject 父子所有权:new QHBoxLayout(this) - 把布局设为 this 的布局,同时隐式接管了 addWidget 进来的子控件。
  • -
-
-
-

常见的坑

-
    -
  • 同一个控件不能同时属于两个布局,重复 addWidget 到不同布局会导致界面 - 异常或崩溃。
  • -
  • 布局设置之后又用 setGeometry/move 手动改控件位置,效果 - 会被布局在下次刷新时覆盖(呼应上一节"控件创建"的坑)。
  • -
-
-
- -
-

自定义控件与布局:QHBoxLayout

-

-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<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), slider, &QSlider::setValue);
-connect(slider, &QSlider::valueChanged, spin, &QSpinBox::setValue);
-    
-

出处:p03/ch08/custom_widget.cpp——布局管理器负责排布, - 不用手算控件坐标。

-
- -
-

QSS:类 CSS 的样式层叠

-
-

核心概念

-
    -
  • QSS 语法脱胎于 CSS:选择器(类型选择器 QPushButton、ID 选择器 - #okButton、伪状态 :hover/:pressed)+ 属性声明, - 但作用对象是 QWidget 树而不是 DOM。
  • -
  • 样式表遵循层叠规则:作用在父控件上的样式默认会被子控件继承,除非子控件有自己更 - 具体的规则覆盖。
  • -
-
-
-

常见的坑

-
    -
  • 用 ID 选择器(#okButton)必须先 setObjectName("okButton"), - 忘记设置会导致规则完全不生效且没有任何报错。
  • -
  • 在某个容器上 setStyleSheet 会级联影响它所有子控件(包括不想改的), - 范围失控是 QSS 最容易踩的坑,通常要用更具体的选择器限定范围。
  • -
-
-
- -
-

QSS 界面美化(教学补充)

-

-okBtn->setObjectName("okButton");
-setStyleSheet(
-    "QPushButton#okButton { background-color: #2f855a; border-radius: 6px; color: white; }"
-    "QPushButton#okButton:hover { background-color: #276749; }"
-);
-    
-

出处:docs/teaching/examples/qss_styling/qt_qss_styling.cpp - (配套 README)—— - 仓库 p03/ 的 wiki 抓取内容没有 QSS,这是本轮补写的教学补充示例,已离屏验证跑通。

-
-
- - -
-
-

模块 B · 练习 95min(计算器案例,练习任务,非预建方案)

-

动手:做一个计算器

+

下午 · 课堂作业 16:00–17:00(工程师答疑)

+

作业:完成计算器

-

任务卡 1 · 计算器雏形

+

任务卡 2 · 简易计算器

目标
-
组合 button_creation.cpp(数字/运算符按钮)+ custom_signal_slot.cpp - (点击信号槽)+ custom_widget.cpp(布局排布按钮),做出一个能加减乘除的最小计算器
+
组合按钮 + 信号槽 + QGridLayout 脚手架,做出能加减乘除的最小计算器
验收标准
-
至少支持两个数 + 一种运算符的计算并在 QLabel/QLineEdit 上正确显示结果; - 界面不重叠、按钮可点击
+
至少支持两个数 + 一种运算符的计算并正确显示结果;界面不重叠、按钮可点击; + connect 全部用新语法且写在构造函数里(今天的坑卡现学现用)
基础知识 · 参考
-
button_creation.cpp · - custom_signal_slot.cpp · - custom_widget.cpp
-
-
-
-
-
-

任务卡 2 · 用 QSS 美化计算器

-
-
目标
-
给任务卡 1 的计算器套一份 QSS 样式(数字键/运算键/等号键区分颜色)
-
验收标准
-
至少 2 类按钮有不同的 background-color;至少 1 个按钮有 - :hover:pressed 效果
-
基础知识 · 参考
-
qt_qss_styling.cpp · - README
+
p03/ch03/button_creation.cpp · + p03/ch04/custom_signal_slot.cpp · + p03/ch08/custom_widget.cpp(布局参考)
@@ -388,15 +303,14 @@ setStyleSheet(

当日产出验证

-

现场演示

-
QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>
-# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口
-

现场演示计算器,至少完成一次正确运算 + QSS 美化。

+

验收

+

现场演示计算器完成至少一次正确运算(QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target> + 或有 X 环境直接运行);中文界面文字无乱码。

- -

返回目录 · - ← 上一天 · +

+

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

diff --git a/docs/teaching/slides/day3.html b/docs/teaching/slides/day3.html index 1199861..e573725 100644 --- a/docs/teaching/slides/day3.html +++ b/docs/teaching/slides/day3.html @@ -2,7 +2,7 @@ -Day 3 · 高级控件 Model/View / 事件机制 / 文件IO与JSON +Day 3 · QMainWindow / QDialog / 布局与控件 / QSS @@ -15,303 +15,211 @@

Day 3

-

高级控件 Model/View / 事件机制 / 文件 IO 与 JSON

-

C++/Qt 方向实训 · 第 3 / 5 天 · 讲:练 ≈ 1:2.7

+

QMainWindow / 对话框 QDialog / 布局管理器 / 常用控件

+

C++/Qt 方向实训 · 第 3 / 4 个讲授日

    -
  • 模块 A(3 课时):Model/View 架构 / 事件机制
  • -
  • 模块 B(3 课时):文件 IO / JSON 解析 → 本地数据存储案例
  • +
  • 上午(08:30–11:00 讲 / 11:00–12:00 练):QMainWindow · 对话框 QDialog
  • +
  • 下午(13:30–16:00 讲 / 16:00–17:00 作业):布局管理器 · 常用控件 · QSS 美化
+

今天的主线:把昨天的计算器升级成"正经软件"——主窗口骨架 + 对话框交互 + + 布局讲透 + 美化。

- +
-

模块 A · 讲授 35min

-

Model/View 架构 / 事件机制

+

上午 · 讲授 08:30–11:00

+

QMainWindow / 对话框 QDialog

-

Model/View:数据与展示分离

+

QMainWindow:标准桌面软件的骨架

核心概念

    -
  • Model/View 把"数据"和"怎么展示数据"彻底拆开:模型(QAbstractItemModel - 及其子类)只负责存数据、回答"这个位置是什么",视图(QListView/ - QTableView/...)只负责画——同一份模型可以同时喂给多个不同视图。
  • -
  • 模型用统一的 QModelIndex(行/列/父项)寻址数据,视图向模型要数据时 - 还要带上"角色"(Qt::DisplayRole 显示文本、Qt::DecorationRole - 图标……),同一个位置可以按不同角色返回不同内容。
  • +
  • QMainWindow 不是"大号 QWidget":它有固定的区域布局协议——菜单栏 / 工具栏 / + 状态栏 / 停靠窗(dock)各就其位,中心部件必须 setCentralWidget() + 显式安放;菜单项和工具栏按钮的统一抽象是 QAction(一个动作, + 多处复用)。

常见的坑

    -
  • 直接改模型内部数据却不发信号(dataChanged/beginInsertRows+ - endInsertRows),视图不会自动刷新,界面和数据"对不上"。
  • -
  • 用了简单的 QStandardItemModel 却又想自定义复杂数据结构:应该继承 - QAbstractItemModel 自己实现,而不是硬塞进 Item 模型。
  • +
  • 直接往 QMainWindow 上摆控件而不设中心部件——布局行为不可预期;
  • +
  • QAction 显示出来 ≠ 生效:忘了 connect 到槽, + 就是"菜单点了没反应"。
+

出处:p03/ch05/central_widget.cpp · + dock_widget.cpp

-

Model/View 模型视图架构(教学补充)

+

QDialog:模态与非模态

+
+

核心概念

+
    +
  • 模态 exec():阻塞等用户给结果,返回值就是用户的选择 + (Accepted/Rejected);非模态 show():立即返回,窗口独立存活 + ——所以对话框对象必须活过当前函数作用域(堆 + parent,或 + WA_DeleteOnClose 自清理)。
  • +
  • 标准对话框拿来就用:QMessageBox(提示/确认)、QFileDialog(选文件)。
  • +
+
+
+

常见的坑(昨天的所有权坑第三次出现!)

+
    +
  • 非模态对话框建在栈上show() 完函数一返回就析构——窗口一闪而过。 + p03/ch06 的三个 modeless 版本(栈/堆/DeleteOnClose)就是这个坑的对照教材。
  • +
+
+

出处:p03/ch06/modal_dialog.cpp · + modeless_dialog_stack.cpp · + modeless_dialog_heap.cpp · + modeless_dialog_deleteonclose.cpp

+
+ +
+

标准对话框速览


-// 模型:只管数据
-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()) { /* ... */ }   // 用户可能点取消:必须处理空返回
     
-

出处:docs/teaching/examples/model_view/qt_model_view_basics.cpp - (配套 README)—— - 仓库 p03/ 的 wiki 抓取内容没有 Model/View,这是本轮补写的教学补充示例,已离屏验证跑通。

-
- -
-

事件循环:事件 vs 信号

-
-

核心概念

-
    -
  • app.exec() 进入的就是事件循环(复用 - Day2 概念)——鼠标点击、 - 键盘输入、定时器触发都会变成"事件"(QEvent),被塞进这个循环里依次分发给 - 对应控件。
  • -
  • 事件和信号是两条不同的机制:事件是"操作系统/框架推给控件"的通知,走 - event()/xxxEvent() 虚函数或事件过滤器;信号槽是"对象之间" - 主动通知,走 connect()。控件收到事件后,常见做法就是在事件处理函数里 - emit 一个信号,把"底层事件"转成"业务语义通知"。
  • -
  • 今天讲的"事件过滤器"就是在事件被分发给控件之前,插一个"拦截点"。
  • -
-

常见的坑

    -
  • 在事件循环所在的主线程做耗时操作会直接卡住整个界面(无法重绘、按钮点了没反应)—— - 这也是 Day4 要讲 QThread 的原因。
  • +
  • 不处理"用户点了取消"(空路径/Rejected)就直接往下用——健壮性问题的高发点。
-
- -
-

事件循环机制(复用 Day2 概念)

-

app.exec() 进入的就是事件循环——鼠标点击、键盘输入、定时器 - 触发都会变成事件,被塞进这个循环里依次分发。今天讲的"事件过滤器"就是在 - 事件被分发给控件之前,插一个"拦截点"。

-

出处:p03/ch02/minimal_qt_app.cpp

-
- -
-

自定义事件处理:核心概念

-
-

核心概念

-
    -
  • 拦截事件有两条路:重写 xxxEvent()(只影响这一个控件自己)或装事件过滤器 - installEventFilter()(可以在事件到达目标控件之前,由另一个对象统一拦截, - 常用于"一个地方管多个控件"的场景)。
  • -
  • 事件对象有 accept()/ignore() 状态:处理完事件要表明"我 - 吃掉了这个事件"还是"继续往上传",决定事件是否会继续传播给父控件。
  • -
-
-
-

常见的坑

-
    -
  • 重写 xxxEvent() 时忘记调用基类实现(如 QWidget::mouseMoveEvent), - 可能丢失控件本身需要的默认行为。
  • -
  • 事件过滤器安装到一个对象上后,如果被观察对象先于过滤器对象销毁(或反过来),要注意 - 生命周期——Qt 会在对象销毁时自动清理,但过滤逻辑里持有的裸指针不会自动清空。
  • -
-
-
- -
-

自定义事件处理 / 事件过滤器

-

-class EventLabel : public QLabel {
-    Q_OBJECT
-public:
-    explicit EventLabel(QWidget* p=nullptr):QLabel(p){ setMouseTracking(true); }
-protected:
-    void mouseMoveEvent(QMouseEvent* event) override {
-        setText(QString("Move: (%1, %2)").arg(event->x()).arg(event->y()));
-    }
-};
-    
-

重写 xxxEvent 是 Qt 事件处理的主线之一(另一条线是事件过滤器 - installEventFilter)。出处:p03/ch09/mouse_event.cpp; - 事件过滤器见 event_filter_mainwindow.cpp

+

出处:p03/ch06/message_box_question.cpp · + message_box_custom.cpp · + file_dialog.cpp

- +
-

模块 A · 练习 95min

-

动手:Model/View + 事件

+

上午 · 课堂练习 11:00–12:00(工程师指导、答疑)

+

动手:计算器主窗口化

-

任务卡 1 · Model/View 换视图

+

任务卡 1 · 给计算器套上 QMainWindow

目标
-
qt_model_view_basics.cpp 里的 QListView 换成 - QTableView,模型代码不改
+
把昨天的计算器改造为 QMainWindow 应用:加菜单「帮助→关于」弹 + QMessageBox;加一个「历史」停靠窗(可先放空 QLabel)
验收标准
-
编译通过,界面正常展示同一份数据,证明"同一个模型可以喂给不同视图"
+
计算器功能不回退;菜单动作有响应;停靠窗可拖动/收起
基础知识 · 参考
-
qt_model_view_basics.cpp · - README
-
-
-
-
-
-

任务卡 2 · 事件过滤器实操

-
-
目标
-
仿 event_filter_mainwindow.cpp,给一个控件装事件过滤器,拦截鼠标按下事件并打印坐标
-
验收标准
-
点击控件后终端/调试输出能看到坐标信息
-
基础知识 · 参考
-
event_filter_mainwindow.cpp · - mouse_event.cpp
+
p03/ch05/central_widget.cpp · + dock_widget.cpp · + p03/ch06/message_box_question.cpp
- +
-

模块 B · 讲授 35min

-

文件 IO / JSON 解析

+

下午 · 讲授 13:30–16:00

+

布局管理器 / 常用控件 / QSS 美化

-

QIODevice:统一的读写抽象

+

布局管理器(回收昨天的脚手架)

核心概念

    -
  • QFile/QTextStream/QDataStream 都建立在 - QIODevice 这一统一读写接口上——同样的"打开/读/写/关闭"套路,以后遇到网络 - 套接字/串口一类设备也是同一套接口,学一次可以迁移到别的设备类型。
  • -
  • QTextStream 面向"文本行"(人可读),QDataStream 面向 - "结构化二进制"(保留类型信息,程序间高效互传但人不可直接读)——按需求选,不是谁更好。
  • +
  • 布局管理器接管子控件的位置/大小计算,随窗口缩放自动重排——昨天计算器里"先用后学" + 的 QGridLayout 今天讲透:QHBox/QVBox/QGrid 三兄弟 + 嵌套组合 + 伸缩因子 + addStretch();布局对象同样遵循 QObject 父子所有权。

常见的坑

    -
  • 忘记检查 open() 的返回值:文件不存在/无权限时静默失败,后续读写在一个 - 没打开的设备上操作,容易读出空数据而不报错。
  • -
  • 混淆文本模式和二进制模式(QIODevice::Text):文本模式下 Qt 会做换行符 - 转换,用来读写二进制数据会破坏内容。
  • +
  • 同一控件不能属于两个布局;
  • +
  • 布局设置后又用 setGeometry()/move() 手动改位置—— + 会被下次布局刷新覆盖,看起来"设置没生效"。
+

出处:p03/ch08/custom_widget.cpp

-

文件系统:QFile 读文本

-

-QFile file("in.txt");
-if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "打开失败"; return -1; }
-while (!file.atEnd()) { qDebug() << file.readLine(); }
-    
-

出处:p03/ch11/qfile_basic.cpp;结构化读写见 - qtextstream_io.cpp、 - qdatastream_io.cpp

-
- -
-

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

+

常用控件 + 资源系统 .qrc

核心概念

    -
  • JSON 读写的本质是"内存对象 ↔ 文本"的一对互逆操作:序列化(QJsonDocument::toJson) - 把内存数据变成可存/可传的文本,反序列化(fromJson)再变回来——正确性的标准是 - "写出去再读回来,数据要一致"(round-trip)。
  • -
  • QJsonValue 是一个变体(variant)类型,能装字符串/数字/布尔/数组/对象, - 取值时要按预期类型调用 toString()/toInt() 等。
  • +
  • QLabel 一个控件三种形态:纯文本 / 富文本 HTML / 图片(QPixmap)/ + 动图(QMovie);输入靠 QLineEdit,按钮族 QPushButton/QRadioButton/QCheckBox。
  • +
  • :/... 路径是 Qt 资源系统:资源编译期打包进可执行文件,运行时不依赖 + 外部文件——发布不用带一堆散图。

常见的坑

    -
  • 忘记检查 QJsonDocument::fromJsonQJsonParseError: - 文本格式错误时返回一个空文档,后续取值全是默认值,不会崩溃也不会报错,很难定位。
  • -
  • 类型不匹配时 toInt()/toString() 等不会抛异常,只会静默 - 返回默认值(0/空串),排查时容易误以为"数据没写进去"。
  • +
  • .qrc 里的路径与代码引用不一致:运行时静默失败(图不显示), + 不报编译错误;忘接 CMake 的 AUTORCC/qt5_add_resources 资源系统不生效。
+

出处:p03/ch08/label_text_html.cpp · + label_pixmap.cpp · + label_movie.cpp

-

JSON 解析与写入(教学补充)

-

-QJsonObject student;
-student["name"] = QStringLiteral("张三");
-student["score"] = 92;
-
-QJsonDocument doc(student);
-QByteArray bytes = doc.toJson(QJsonDocument::Indented);   // 序列化
-
-QJsonDocument parsed = QJsonDocument::fromJson(bytes);     // 反序列化
-QJsonObject obj = parsed.object();
-qDebug() << obj["name"].toString() << obj["score"].toInt();
-    
+

QSS 界面美化

+
QPushButton#okButton { background-color: #2f855a; border-radius: 6px; }
+QPushButton#okButton:hover { background-color: #276749; }
+
+

核心概念

+
    +
  • 语法脱胎于 CSS:类型选择器 / ID 选择器(#objectName)/ 伪状态 + (:hover :pressed)+ 层叠继承——作用对象是 QWidget 树而非 DOM; + "长什么样"与"做什么"分离,改样式不碰逻辑。
  • +
+
+
+

常见的坑

+
    +
  • ID 选择器必须先 setObjectName()
  • +
  • setStyleSheet() 会级联影响所有子控件——范围失控是最容易踩的坑, + 样式尽量写窄(带选择器),不要裸写属性。
  • +
+

出处:docs/teaching/examples/json_io/qt_json_parse_write.cpp - (配套 README)—— - 仓库 p03/ch11 的 wiki 抓取内容只有 QFile/QTextStream/QDataStream,没有 JSON, - 这是本轮补写的教学补充示例;不需要 GUI,tools/run_qt.sh qt_json_parse_write - 直接跑即可,不需要 QT_QPA_PLATFORM=offscreen

+ href="../examples/qss_styling/qt_qss_styling.cpp">examples/qss_styling/qt_qss_styling.cpp + (README

- +
-

模块 B · 练习 95min(本地数据存储案例,练习任务,非预建方案)

-

动手:本地数据存储小工具

+

下午 · 课堂作业 16:00–17:00(工程师答疑)

+

作业:计算器美化升级

-

任务卡 1 · JSON 落盘

+

任务卡 2 · 用 QSS 美化计算器

目标
-
qt_json_parse_write.cpp 的 JSON 数据实际写入一个文件(结合 - qfile_basic.cppQFile 写法),再读回来解析校验
+
数字键/运算键/等号键用 QSS 区分颜色;整体布局改为随窗口缩放自适应
验收标准
-
程序运行后磁盘上生成一个 .json 文件,内容可读;重新运行程序能正确解析出 - 之前写入的数据
+
至少 2 类按钮有不同 background-color;至少 1 个按钮有 + :hover:pressed 效果;拉伸窗口按钮跟着重排
基础知识 · 参考
-
qt_json_parse_write.cpp · - README · - qfile_basic.cpp
-
-
-
-
-
-

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

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

当日产出验证

-

现场演示

-

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

+

验收

+

现场演示升级后的计算器:主窗口(菜单/停靠窗)+ QSS 配色 + 缩放自适应,功能不回退。

- -

返回目录 · - ← 上一天 · +

+

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

diff --git a/docs/teaching/slides/day4.html b/docs/teaching/slides/day4.html index eb6c992..13bdb52 100644 --- a/docs/teaching/slides/day4.html +++ b/docs/teaching/slides/day4.html @@ -2,7 +2,7 @@ -Day 4 · 2D绘图/多线程/动画框架与状态机 +Day 4 · 事件机制 / 绘图与表格 / 文件系统 @@ -15,278 +15,250 @@

Day 4

-

2D 图形绘制 / 多线程 / 动画框架与状态机

-

C++/Qt 方向实训 · 第 4 / 5 天 · 讲:练 ≈ 1:2.7

+

Qt 消息机制和事件 / 绘图事件及表格事件 / 文件系统

+

C++/Qt 方向实训 · 第 4 / 4 个讲授日

    -
  • 模块 A(3 课时):2D 绘图 / 多线程
  • -
  • 模块 B(3 课时):动画框架 / 状态机 → 交互式小游戏案例
  • +
  • 上午(08:30–11:00 讲 / 11:00–12:00 练):事件循环与派发 · 事件拦截 · 绘图事件 · 表格
  • +
  • 下午(13:30–16:00 讲 / 16:00–17:00 作业):文件系统(QFile/QTextStream/QDataStream + JSON)
+

今晚的作业「本地数据存储小工具」是明天 Day5 综合案例的直接前身。

- +
-

模块 A · 讲授 35min

-

2D 绘图 / 多线程

+

上午 · 讲授 08:30–11:00

+

事件机制 / 绘图事件 / 表格事件

-

QPainter:由 Qt 驱动的重绘模型

+

事件循环与派发:事件 vs 信号

核心概念

    -
  • Qt 的绘制是"框架驱动"而非"你想画就画":所有绘制只能发生在 paintEvent - 里,由 Qt 在需要重绘时(窗口刚显示、被遮挡后重新露出、你主动请求)自动调用——这和直接式 - 绘图(想画就立刻画到屏幕)是不同的模型。
  • -
  • 控件想主动触发重绘要调用 update()(异步,排队到下次事件循环处理)而不是 - 直接调用 paintEvent()QPainter 只是"在哪个设备上画"的画笔, - 画布可以是控件本身,也可以是 QImage/QPixmap 等离屏设备。
  • +
  • Day2 的 app.exec() 今天拆开看:事件循环不停从队列取 QEvent + 派发给目标控件——事件是框架推给控件的通知(鼠标/键盘/重绘),信号槽是 + 对象间的主动通知,两套机制并存不冲突。
  • +
+
+
+

常见的坑(明天的伏笔)

+
    +
  • 主线程做耗时操作 = 事件循环停摆 = 整个界面冻结——点不动、拖不动、白屏。 + 这是学员项目里最高频的"卡死"来源,正确解法(多线程)明天综合案例专门讲。
  • +
+
+

出处:p03/ch02/minimal_qt_app.cppexec()

+
+ +
+

事件拦截:重写 xxxEvent vs 事件过滤器

+
+

核心概念

+
    +
  • 两条拦截路径:重写 mousePressEvent() 等虚函数(只影响自己); + installEventFilter()(一个对象统一拦截多个控件的事件)。 + 事件有 accept()/ignore() 状态,决定是否继续向父控件传播。

常见的坑

    -
  • paintEvent 之外的地方直接 new QPainter(this) 画东西: - 要么无效要么未定义行为,绘制必须在 paintEvent 回调期间进行。
  • -
  • 数据变了却忘记调用 update():界面不会自动感知"要重画了",看起来像是 - "画错了",其实是"根本没重画"。
  • +
  • 重写事件函数忘记调用基类实现(QWidget::mousePressEvent(e))—— + 控件默认行为悄悄丢失,现象诡异难查。
+

出处:p03/ch09/event_override_customwidget.cpp · + event_filter_mainwindow.cpp · + mouse_event.cpp

-

绘图:QPainter

-

-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));
-}
-    
-

所有绘制都发生在 paintEvent 里,由 Qt 在需要重绘时自动调用。 - 出处:p03/ch10/qpainter_basic.cpp;绘图设备 - (QImage/QPixmap/QPicture)见 - paint_device_qimage.cpp、 - paint_device_pixmaps.cpp

-
- -
-

QThread:线程亲和性与 worker-object

+

绘图事件:QPainter

核心概念

    -
  • "线程亲和性"(thread affinity):每个 QObject 都归属于创建它/被 - moveToThread 移动到的那个线程,它的槽函数默认在所属线程执行——GUI 控件必须 - 始终归属主线程,这是 Qt 线程模型的核心约束。
  • -
  • 官方推荐的是 worker-object 模式(把耗时逻辑放进一个普通 QObject, - moveToThread 到工作线程),而不是继承 QThread 重写 - run()——前者能继续用信号槽通信,后者容易把线程亲和性搞混。
  • -
  • 跨线程的信号槽连接会自动变成 Queued(回顾 - Day2),结果通过事件循环 - 安全地"带回"接收者所在线程,不需要手动加锁。
  • +
  • Qt 绘制是框架驱动而非"想画就画":所有绘制只能发生在 paintEvent() + 里,由 Qt 按需调用;数据变了想重画,调 update()(异步排队一次重绘请求)。 + 绘图设备三件套 QImage/QPixmap/QPicture 作参考。

常见的坑

    -
  • 在工作线程里直接操作 GUI 控件(比如直接 label->setText()):Qt 的 GUI - 模块不是线程安全的,这是未定义行为/崩溃的常见来源——正确做法永远是发信号,排队回主线程处理。
  • -
  • moveToThread 要在 connectthread.start() - 之前完成;对象一旦有耗时任务在跑,中途再挪线程会有竞态。
  • +
  • paintEvent 之外直接构造 QPainter 画窗口——未定义行为;
  • +
  • 数据变了忘调 update()——界面不会自动重画。
+

出处:p03/ch10/qpainter_basic.cpp + (设备类参考:paint_device_qimage.cpp 等)

-

QThread 多线程(教学补充)

-

-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);
-    
+

表格:QTableWidget 与单元格事件

+
+

核心概念

+
    +
  • item 类控件:数据直接塞进控件(QTableWidgetItem,new 出来交给表格 + 接管所有权),上手快、中小项目够用;"表格事件"以信号暴露: + cellClicked / cellDoubleClicked / itemChanged
  • +
+
+
+

钩子 · 它背后是完整的 Model/View 架构

+
    +
  • 数据(model)与展示(view)彻底分离、同一模型可喂多个视图(QListView/QTableView); + 改模型必须发 dataChanged 等信号视图才会刷新。数据规模大/多视图联动时的 + 正解,自学入口:examples/model_view/
  • +
+

出处:docs/teaching/examples/qthread_worker/qt_thread_worker.cpp - (配套 README)—— - 仓库 p03/ 的 wiki 抓取内容没有多线程,这是本轮补写的教学补充示例,已验证跑通。

+ href="../examples/table_widget/qt_table_widget_basics.cpp">examples/table_widget/qt_table_widget_basics.cpp + (README

- +
-

模块 A · 练习 95min

-

动手:绘图 + 多线程

+

上午 · 课堂练习 11:00–12:00(工程师指导、答疑)

+

动手:拦事件 + 画图形

-

任务卡 1 · 画一个简单图形界面

+

任务卡 1 · 事件过滤器 + 自绘图形

目标
-
仿 qpainter_basic.cpp,在 paintEvent 里画一个组合图形 - (如棋盘格/仪表盘刻度)
+
① 给一个控件装事件过滤器,拦截鼠标按下并打印坐标; + ② 在 paintEvent 里画一个组合图形(棋盘格或仪表盘刻度)
验收标准
-
编译通过,离屏或 X 环境下能看到图形正确绘制(可截图验证)
+
点击控件终端能看到坐标输出;图形正确绘制(可截图验证)
基础知识 · 参考
-
p03/ch10/qpainter_basic.cpp
-
-
-
-
-
-

任务卡 2 · 感受多线程不卡界面

-
-
目标
-
qt_thread_worker.cpp 里的耗时计算换成一个更长的循环或 - QThread::msleep,观察主线程/事件循环没有被卡住
-
验收标准
-
能说清楚"如果把这段计算直接放主线程会发生什么"(界面无响应),并解释 - moveToThread 起的作用
-
基础知识 · 参考
-
qt_thread_worker.cpp · - README
+
p03/ch09/event_filter_mainwindow.cpp · + p03/ch10/qpainter_basic.cpp
- +
-

模块 B · 讲授 35min

-

动画框架 / 状态机

+

下午 · 讲授 13:30–16:00

+

文件系统:QFile / 流 / JSON

-

QPropertyAnimation:动画建立在属性系统上

+

QIODevice 统一读写抽象

核心概念

    -
  • Qt 的属性系统(Q_PROPERTY,同样靠 - Day2 讲过的 moc 生成)给每个属性配了 - 统一的 get/set 接口;动画框架的本质就是"在一段时间内,用缓动曲线不断调用某个属性的 - setter",所以任何声明成 Q_PROPERTY 的属性(不只是内置的 geometry/opacity) - 都能被动画驱动。
  • -
  • QEasingCurve 决定的是"数值随时间变化的曲线形状"(线性/回弹/加减速……), - 动画本身只是"定时器 + 插值 + 调 setter"的组合,理解这点能解释为什么几乎所有属性都能做动画。
  • +
  • QFile/QTextStream/QDataStream 都建立在 + QIODevice 统一接口上——之后遇到网络/串口也是同一套; + QTextStream 面向文本行,QDataStream 面向结构化二进制。

常见的坑

    -
  • 要动画化的属性必须是真正的 Q_PROPERTY(有 setter),随手传一个字符串 - 属性名如果对应类没有声明该属性,动画不会报错,只是什么也不会发生。
  • -
  • 动画作用的对象在动画播放完成前被销毁会导致悬空访问; - QAbstractAnimation::DeleteWhenStopped 能让动画结束后自动释放自身,但不管 - 目标对象的生命周期,目标对象仍需自己保证不提前销毁。
  • +
  • open() 返回值不检查——文件不存在/没权限时静默失败, + 后续读写全是空操作;
  • +
  • 文本/二进制模式混淆会破坏内容。
+

出处:p03/ch11/qfile_basic.cpp · + qtextstream_io.cpp · + qdatastream_io.cpp

-

QPropertyAnimation 动画(教学补充)

-

-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);
-    
-

出处:docs/teaching/examples/property_animation/qt_property_animation.cpp - (配套 README

-
- -
-

QStateMachine:状态机建立在信号槽上

+

JSON 解析与写入

核心概念

    -
  • 状态机解决的是"同一个控件在不同状态下行为/外观不同"的问题——与其用一堆 - bool 标志位加 if-else 判断当前处于什么状态,不如让"状态"本身成为一等对象 - (QState),每个状态自己声明"进入这个状态要怎样"(assignProperty, - 又用到了 Qt 属性系统)。
  • -
  • 状态之间的切换(addTransition)本质是"信号触发的转移"——转移条件就是 - 一个信号,这让状态机和信号槽自然衔接:任何能发信号的事件都能驱动状态切换。
  • +
  • 序列化/反序列化是一对互逆操作,正确性标准是 round-trip:写出去再读回来 + 数据一致;QJsonDocument/Object/Array/Value 四件套, + QJsonValue 是能装多种类型的变体。

常见的坑

    -
  • 忘记 machine.setInitialState(...) 或忘记调用 machine.start(): - 状态机对象存在但从未真正运行,所有 assignProperty/addTransition - 都不会生效。
  • -
  • 某个状态没有覆盖所有可能触发的信号/条件,会导致状态机"卡住"——设计状态机前先把所有 - 状态和转移条件画成图,比直接写代码更不容易漏。
  • +
  • 不检查 QJsonParseError:格式坏的输入静默变成空文档
  • +
  • 类型不匹配时 toInt()/toString() 不报错只返回默认值—— + 数据悄悄丢,很难查。
  • +
+
+

出处:examples/json_io/qt_json_parse_write.cpp + (README

+
+ +
+

★ 相对路径与工作目录

+

+QFile bad("data.json");                       // ✗ 相对当前工作目录——它在哪取决于你怎么启动
+QFile good(QCoreApplication::applicationDirPath() + "/data.json");   // ✓ 锚定可执行文件位置
+    
+
+

核心概念

+
    +
  • 相对路径基于进程当前工作目录——不是源码目录,也不一定是可执行文件目录; + IDE、终端、脚本三种启动方式的工作目录可能各不相同。正解: + applicationDirPath() 拼路径,或只读资源走 .qrc(昨天的资源卡)。
  • +
+
+
+

常见的坑

+
    +
  • "IDE 里能读到文件,命令行跑就说不存在"——几乎每届学员必问,根源就是这页。
-

QStateMachine 状态机(教学补充)

+

★ 文本编码再遇(文件版)


-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 默认编码随平台/区域设置而变——跨机器必须显式指定
     
-

出处:docs/teaching/examples/state_machine/qt_state_machine.cpp - (配套 README)—— - QStateMachine/QState 属于 QtCore,不需要额外模块。以上两个 - (动画、状态机)都是仓库 p03/ 原本没有的教学补充示例,已离屏验证跑通。

+
+

核心概念

+
    +
  • Day2 界面乱码(QString/源码编码)和今天文件乱码是同一根源的两次出现: + 字节 ↔ 文字之间永远隔着一层"编码",跨边界(源码/文件/网络)就必须显式指定。
  • +
+
+
+

常见的坑

+
    +
  • Windows 记事本写的文件 Linux 程序读出乱码(或反过来)——不是"文件坏了", + 是两边默认编码不同;养成条件反射:编码要显式指定
  • +
+
- +
-

模块 B · 练习 95min(交互式小游戏案例,练习任务,选做,非预建方案)

-

动手:动画 + 状态机小游戏

+

下午 · 课堂作业 16:00–17:00(工程师答疑)

+

作业:本地数据存储小工具

-

任务卡 1 · 动画 + 状态机组合

+

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

目标
-
把动画和状态机组合到同一个窗口——例如按钮在"红灯"状态时禁止移动,"绿灯"状态时 - 点击才触发动画
+
设计「学员信息」结构(姓名/成绩/标签),支持增加记录并保存到 JSON 文件, + 重启程序后能读出之前保存的全部记录
验收标准
-
状态切换正确(红灯不触发动画,绿灯触发动画);动画播放完整不报错
+
至少能存 2 条以上记录并在重启后完整读回;字段不丢失;数据文件路径用 + applicationDirPath() 拼(今天的坑卡现学现用)
基础知识 · 参考
-
property_animation/ · - state_machine/
-
-
-
-
-
-

任务卡 2(选做)· 简易反应力小游戏

-
-
目标
-
结合任务卡 1 + 鼠标事件(Day3 mouse_event.cpp)+ 多线程计时 - (Day4 qt_thread_worker.cpp),做一个"状态变化后限时点击"的最小交互游戏
-
验收标准
-
游戏至少有"开始→进行中→结束"三个可辨识状态,计时或计分逻辑不崩溃
-
基础知识 · 参考
-
上述三个教学补充示例 + - mouse_event.cpp
+
examples/json_io/ · + p03/ch11/qfile_basic.cpp
@@ -296,15 +268,12 @@ machine.start();

当日产出验证

-

现场演示

-
QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>
-# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口
-

分组演示动画+状态机组合小工具(任务卡 2 选做,量力而行)。

+

验收

+

现场演示「写入 → 重启 → 读回」完整闭环;讲得出 open()/ParseError 两处检查为什么不能省。

- -

返回目录 · - ← 上一天 · +

+

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

diff --git a/docs/teaching/slides/day5.html b/docs/teaching/slides/day5.html index 1eddc8f..4d91531 100644 --- a/docs/teaching/slides/day5.html +++ b/docs/teaching/slides/day5.html @@ -2,7 +2,7 @@ -Day 5 · 团队协同开发/综合案例复盘/OpenCV可选 +Day 5 · 综合项目案例讲解:学员信息管理器 @@ -15,198 +15,204 @@

Day 5

-

团队协同开发 / 综合案例复盘 / OpenCV(可选自学)

-

C++/Qt 方向实训 · 第 5 / 5 天 · 最后一天课堂内容

+

综合项目案例讲解:学员信息管理器(全天)

+

C++/Qt 方向实训 · 综合案例日 · 明天起进入项目开发

    -
  • 模块 A(3 课时):团队协同开发(概念讲解)
  • -
  • 模块 B(3 课时):综合案例复盘 + OpenCV/AI(可选,自学指针)
  • +
  • 上午:需求拆解 → 界面骨架 → 数据层(Day1–4 全部知识点的收束)
  • +
  • 下午:提升——QThread 防卡界面;开发流程与规范;分组定项目方向
-

今天结束后 Day6–10 是纯项目实作(学员编码 + 教员指导 + 成果展示), - 不再有课堂讲授——今天的分组结论会直接带到项目实作阶段。

+

目标:通过一个完整小软件讲清 Qt 开发的流程、规范及方法—— + 项目 = 已学知识点的组装。参考实现: + examples/student_manager/ + (README)。 + Day6–9 学员项目开发,Day10 下午答辩。

- +
-

模块 A · 讲授 35min

-

团队协同开发

+

上午 1 · 需求拆解 → 界面骨架

+

从需求到窗口

-

质量保证:四平台一标准(复习)

- -

出处:docs/TRAINING_PLAN_2026.md 「七、质量保证 (二) Git」

+

需求拆解:先想清楚再动手

+
+

核心概念——开发流程的第一步不是写代码

+
    +
  • 需求:管理学员记录(姓名/成绩/标签),支持添加、保存、读取、批量导入。
  • +
  • 拆功能 → 映射到已学组件:列表展示=QTableWidget(Day4)、添加=模态对话框(Day3)、 + 保存读取=QFile+JSON(Day4)、批量导入=后台线程(今天新授)、外壳=QMainWindow(Day3)。
  • +
  • 拆完就得到任务清单——这正是 Day6 起你们在 Jira 上要做的事。
  • +
+
-

Git 提交习惯:核心概念

+

界面骨架走查

+
+

回指 Day3–4

+
    +
  • QMainWindow:菜单(文件/记录/帮助)+ 工具栏 + 状态栏 + setCentralWidget(table)
  • +
  • QAction 一个动作三处复用(菜单/工具栏/快捷键),每个 action 都要 connect
  • +
  • 添加记录:模态 exec() + QFormLayout + QDialogButtonBox
  • +
  • 表格 item 所有权:new 出来交给表格——父子所有权模型第 N 次出现。
  • +
+
+

现场跑:tools/run_qt.sh qt_student_manager(离屏自测版: + QT_QPA_PLATFORM=offscreen,自动走完整验证流程)

+
+
+ + +
+
+

上午 2 · 数据层

+

记录 ↔ JSON ↔ 磁盘

+
+ +
+

数据层走查

+

+struct Student { QString name; double score; QString tag; };
+QJsonObject toJson(const Student&);      // 互逆的一对
+Student fromJson(const QJsonObject&);    // round-trip = 正确性标准
+    
+
+

回指 Day4(昨天作业的完整版)

+
    +
  • 数据结构与界面分离:Student 结构体不知道 QTableWidget 的存在—— + 这是 Model/View 思想的朴素版;
  • +
  • 健壮性三处检查全部在场:open() 返回值、QJsonParseError、 + 路径用 applicationDirPath() 拼。
  • +
+
+
+

复盘昨天作业的常见问题

+
    +
  • 保存和读取的字段名拼写不一致("score" vs "Score")——round-trip 一测就露馅, + 这就是为什么验收标准是"写进去读出来一致"而不是"能生成文件"。
  • +
+
+
+
+ + +
+
+

下午 1 · 提升:多线程防卡界面(新授)

+

QThread:耗时操作不冻结界面

+
+ +
+

先看病症:把耗时活儿放主线程

+
+

现场演示

+
    +
  • msleep 循环直接放按钮槽里 → 整个窗口冻结:点不动、拖不动、 + 系统提示"未响应"——Day4 讲过:主线程阻塞 = 事件循环停摆。 + 这是学员项目里最高频的"卡死"来源。
  • +
+
+
+ +
+

正解:worker-object 模式

+

+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();
+    
+
+

核心概念(三件套)

+
    +
  • 线程亲和性:每个 QObject 归属一个线程,GUI 控件必须归主线程;
  • +
  • worker 对象:官方推荐做法——耗时逻辑放 worker,moveToThread + 搬进工作线程,而不是继承 QThread 重写 run;
  • +
  • 跨线程信号槽自动 Queued(Day2 的连接方式今天闭环):worker 发信号、 + 主线程槽更新表格,不需要手动加锁。
  • +
+
+
+

常见的坑

+
    +
  • 在工作线程直接操作 GUI 控件——未定义行为,跨线程崩溃的头号来源;
  • +
  • moveToThread 写在 start() 之后 / worker 带了 parent——都不生效。
  • +
+
+

最小版:examples/qthread_worker/ · + 案例集成版:student_manager 的 startImport()

+
+
+ + +
+
+

下午 2 · 开发流程与规范 + 分组定方向

+

进项目前的最后一课

+
+ +
+

Git 提交习惯

核心概念

    -
  • 接下来 Day6–10 项目实作,每人每完成一个任务都要提交代码到 Git 并写清任务描述—— - 提交粒度对应 Jira 任务粒度,commit 信息就是"这次提交做了什么、为什么"的可追溯记录。
  • +
  • 每完成一个任务就提交,提交粒度对应任务粒度;commit 信息说清"做了什么、为什么"—— + 它是项目的可追溯记录,评审和回滚都靠它。

常见误区

    -
  • 攒了几天代码一次性提交、commit 信息写"update":历史记录失去可追溯性,出问题时无法 - 定位是哪次改动引入的。
  • +
  • 攒几天代码一次性提交、信息写"update"——历史失去可追溯性,出问题无法定位是哪次 + 改动引入的。从 Day6 的第一次 commit 开始就按规范来。
-

标准化考核标准

- -

出处:docs/TRAINING_PLAN_2026.md 「七、(五) 标准化的考核标准」

-
- -
-

考核标准:核心概念

+

考核标准与分组定方向

核心概念

    -
  • 项目管理评估(40%) + 项目技术评估(30%) + 职业素养评估(30%)——技术实现只占三成, - Day6–10 项目实作最终按这三个维度综合打分,不是"代码能跑就满分"。
  • +
  • 考核不只看代码:项目管理(任务拆解/进度可见)+ 项目技术 + 职业素养(协作/答辩表达) + 多维度评估——只埋头写代码会丢掉其他维度的分。
  • +
  • 今天散场前每组确认:项目选题 + 初步分工 + 任务拆解草稿(上午"需求拆解"那页 + 就是模板)。
-
-

常见误区

-
    -
  • 只顾埋头写代码、忽略项目管理和职业素养维度(如任务跟踪是否及时更新、沟通是否 - 到位):技术分再高,另外 70% 的权重会明显拉低总评。
  • -
-
-
-
- - -
-
-

模块 A · 讨论 95min

-

动手:分工复盘

-
-
-
-

任务卡 1 · 分工复盘

-
-
目标
-
结合考核标准图(项目管理评估/项目技术评估/职业素养评估三大块),对照 Day1 - 任务卡 1 定下的分工,检查执行情况
-
验收标准
-
每组能说出至少 1 项做得好的、1 项需要改进的
-
基础知识 · 参考
-
上一节"标准化考核标准"图 · Day1 任务卡 1 的分工结论
-
-
-
-
- - -
-
-

模块 B · 讲授 35min

-

综合案例复盘 + OpenCV/AI(可选)

-
- -
-

Day2–4 案例回顾

-

请 1–2 组现场展示计算器 / 本地数据存储 / 动画状态机小游戏,其他组观摩提问。

-
-

核心概念

-
    -
  • 代码走查(code review)是团队协作的常规环节——展示时讲清"为什么这么设计"比"功能能跑" - 更能体现对 Day2–4 核心概念(对象树/信号槽/Model-View/线程亲和性……)的理解程度。
  • -
-
-
-

常见误区

-
    -
  • 只演示"能跑"不讲"为什么":观摩组提不出有效问题,复盘就失去了巩固概念的作用。
  • -
-
-
- -
-

OpenCV 与 AI 机器学习(可选,自学指针)

-
-

核心概念

-
    -
  • OpenCV 是独立于 Qt 的计算机视觉库,Qt 负责界面/交互,OpenCV 负责图像算法——两者通过 - "内存里的图像数据格式转换"(cv::MatQImage)配合,是很多 - 桌面视觉类项目的典型组合方式。
  • -
-
-
-

缺口说明

-

方案书原文对本项标注"可选择",属于非必修拓展。引入 OpenCV 会新增一条完整的 - 三方库工具链,本仓库暂不提供可运行示例。如果感兴趣,自学方向是 - cv::CascadeClassifier 人脸检测入门。

-
-

OpenCV 官方教程(doc.opencv.org,选读·需联网)

-
-
- - -
-
-

模块 B · 练习 95min(综合演练)

-

动手:三选一深化

-
-
-
-

任务卡 1 · 三选一深化

-
-
目标
-
从计算器 / 本地数据存储 / 动画状态机小游戏中选一个继续深化(加一个新功能)
-
验收标准
-
现场演示新增功能正常工作,编译无报错
-
基础知识 · 参考
-
回顾 Day2–4 对应的教学补充示例与 README: - qss_styling · - json_io · - property_animation · - state_machine
-
+
+

兴趣钩子(各 5 分钟演示,选学)

+

给界面加动效:QPropertyAnimation(任何 + Q_PROPERTY 都能被缓动曲线驱动) · + 多状态 UI:QStateMachine(状态成为一等对象, + 替代一堆 bool 标志)· 线程的更高抽象:QtConcurrent/线程池。

-

当日产出验证 · Day1–5 阶段总验收

+

当日产出验证 · 进入项目期

-

现场演示

-

每组现场演示深化后的案例,教师按考核标准图三大块给出反馈,为 Day6–10 - 的正式项目实作分组定方向。

+

验收

+
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_student_manager
+# 自测 1/2:JSON 写入→清空→读回,2 条记录完整
+# 自测 2/2:后台线程导入 3 条,总计 5 条,界面未阻塞
+

每组提交:项目选题 + 分工说明 + 任务拆解草稿。明天 Day6 起:学员编码 + 工程师指导, + Day10 下午项目答辩。

- -

返回目录 · - ← 上一天

-
- - -
-

延伸阅读 · Day6–10 项目实作支撑知识体系

-
-

16 个可选项目,除 C++/Qt 之外还需要哪些知识?

-

系统梳理了实现这 16 个项目所需的 12 个辅助专业领域——数据结构与算法、 - 2D 游戏与碰撞、计算机图形学、数字图像处理、计算机视觉与机器学习(OpenCV/Haar/LBPH)、 - 编译原理与状态机、编码理论、计算机网络、并发编程、文件系统与增量同步、软件设计模式等, - 每个核心知识点附 2–3 个已验证可用的网络课程链接(中英文兼顾)。

-

全文(serve.py 下本地一点即开):docs/teaching/SUPPORTING_KNOWLEDGE.md -  →  qt-course-projs/docs/supporting-knowledge.md

-
- -

返回目录 · - ← 上一天

+ +

返回目录 · ← 上一天

diff --git a/docs/teaching/slides/index.html b/docs/teaching/slides/index.html index 41b6528..e26f9ce 100644 --- a/docs/teaching/slides/index.html +++ b/docs/teaching/slides/index.html @@ -2,7 +2,7 @@ -C++/Qt 方向实训 · 5 天教案幻灯片 +C++/Qt 方向实训 · 4+1 天教案幻灯片 -

川大 C++/LinuxC 课程「02. Qt 方向」—— 5 天实训幻灯片

-

对齐官方《四川大学软件学院 2026 年夏 C/C++ 项目实习实训方案书 V1.2》 - 「五、执行计划 (二) C++方向」Day1–5(Day6–10 是纯项目实作,无课堂内容,不在本教案范围)。 - 配套大纲:docs/teaching/OUTLINE.md; - 方案书全文:docs/TRAINING_PLAN_2026.md。 - 离线可用(reveal.js 已 vendor 进仓库);代码链接需通过本地 HTTP 服务打开才能正常跳转, - 见仓库 docs/teaching/OUTLINE.md 「材料与播放方式」一节。

+

川大 C++/LinuxC 课程「02. Qt 方向」—— 讲授 4 天 + 综合案例 1 天

+

对齐 2026-07 新版 10 天实训安排(Day1–4 讲授、Day5 综合案例讲解、 + Day6–10 学员项目开发与答辩)。配套大纲: + docs/teaching/OUTLINE_4DAY.md + (含新安排简表、逐天设计与中初级学员适配性评估); + 旧 5 天版大纲 OUTLINE.md 保留作历史参照。 + 离线可用(reveal.js 已 vendor 进仓库);代码链接需通过本地 HTTP 服务打开才能正常跳转 + (python3 -m http.server 8000,见大纲「材料与播放方式」)。