diff --git a/docs/teaching/OUTLINE.md b/docs/teaching/OUTLINE.md index 45c959d..90420a2 100644 --- a/docs/teaching/OUTLINE.md +++ b/docs/teaching/OUTLINE.md @@ -40,6 +40,15 @@ - 如果后续需要这两项也做成可运行示例,请明确提出——这会涉及新增外部依赖, 需要单独确认。 +**核心概念覆盖原则**(本轮追加):此前一版的"讲"部分基本只是代码演示(贴代码 + 一句出处 +说明),没有讲透知识点本身的原理。本轮改为:**每个"讲"知识点在大纲里至少固定两项——核心 +概念(是什么/为什么,不只是怎么写)+ 常见的坑(注意事项/误区)**,随知识点内联在下面各天 +的表格后。这是**覆盖清单**而非固定讲稿:课堂实际讲解深度按学员反馈现场调整(PRETEST 摸底 +好、进度快,可以点到为止;反馈吃力,可以就某个概念多展开几分钟),但大纲必须列全这些点, +以免讲课时漏掉、或降级回退到纯代码演示。幻灯片 `day1.html`…`day5.html` 每个知识点前都配了 +对应的"核心概念/常见的坑"卡片(`custom.css` 里的 `.concept-box`/`.pitfall-box`),大纲里的 +条目和幻灯片内容一一对应,改一处要同步改另一处。 + ## 1. 总体目标 1. 学员能说出 C++ 相对 C 的关键扩展机制在**哪个标准版本**成立/失效(复习向, @@ -92,6 +101,15 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | 敏捷 Scrum 项目开发管理 | 同上「服务流程图」里的迭代环节(需求分析→需求评审→项目研发→阶段评审→项目测试) | | C++ 在企业中的应用 | 口头介绍,结合往届案例(如有) | +**核心概念与常见误区**: + +- **实习实训服务流程**:概念——需求分析→需求评审→项目研发/代码实现→阶段评审→项目测试/交付, + 每环节都有明确"评审"节点把关;误区——把需求分析当走过场,跳过评审直接编码,后期返工成本 + 远高于前期对齐需求的成本。 +- **四平台一标准**:概念——JIRA/Git/Wiki/OASIS 四个工具分别对应"谁在做什么/代码怎么变化/ + 知识怎么沉淀/进度怎么被看到"四个维度,缺一个都有协作盲区;误区——只用 Git 不用 Jira,代码 + 有版本历史但任务分工和进度没有可见记录。 + **讨论(95min,分组,非编码)**: - **任务卡 1** @@ -117,6 +135,25 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | CMake 项目构建 | 本仓库 `CMakeLists.txt`、`p03/CMakeLists.txt` | 用真实项目当教材:`option(BUILD_QT_PART)`、`add_subdirectory`、每个示例一个 `add_executable` 目标 | | Vcpkg 包管理器(概念,无演示) | 无仓库代码 | C++ 生态的包管理器,解决"第三方库怎么装、怎么被 CMake 找到"的问题;本仓库用的是"隔离 Qt 手动装"而非 vcpkg,两种思路都要认识 | +**核心概念与常见的坑**(每个知识点至少覆盖这两项,课堂讲解深度按学员反馈调整;对应幻灯片 +`day1.html` 每个知识点前的"核心概念/常见的坑"卡片): + +- **namespace 作用域**:概念——作用域可跨文件延续、跨块合并,不是"文件顺序";坑——头文件里 + `using namespace std;` 会把命名空间污染带给所有 include 它的地方。 +- **函数重载决议**:概念——编译器靠名字改写(name mangling)区分重载,二义性源于"同样好"的 + 候选而非代码写错;坑——隐式类型转换会扩大候选集,可能触发意料之外的重载版本。 +- **引用的本质**:概念——"自动解引用的 const 指针"这一视角能解释必须初始化、不能重新绑定; + 坑——返回局部变量的引用,函数结束后变成悬空引用。 +- **虚析构与内存泄漏**:概念——基类指针 delete 派生类对象时,只有虚析构才会走虚函数表调用到 + 派生类析构;坑——只要类可能被继承+可能被基类指针删除,就该把析构声明为 virtual。 +- **模板与 STL 速览**:概念——泛型编程让代码脱离具体类型复用,迭代器是容器与算法之间的统一 + 接口;坑——容器扩容/删除元素后旧迭代器可能失效,继续用是未定义行为。 +- **CMake 项目构建**:概念——CMake 是"生成构建系统的工具"而非构建系统本身,`-S -B` 生成、 + `--build` 才是真正编译;坑——改了生成器相关配置(换生成器/清缓存)后要重新走一次 `-S -B`。 +- **Vcpkg 包管理器**:概念——一次 `vcpkg install` 配合 CMake toolchain 文件,`find_package` + 即可自动定位头文件和库;坑——不是所有第三方库都必须用包管理器装,本仓库选择"隔离 Qt 手动装" + 是精确控制版本/路径的另一种取舍,两种思路都要认识。 + **练(95min)**: - **任务卡 1 · 走一遍真实构建流程** @@ -149,6 +186,19 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | 控件创建与显示 | `p03/ch03/button_creation.cpp` | | 内置 / 自定义 / lambda 信号槽 | `p03/ch04/builtin_signal_slot.cpp`、`custom_signal_slot.cpp`、`lambda_signal_slot.cpp` | +**核心概念与常见的坑**(对应幻灯片 `day2.html` 每个知识点前的"核心概念/常见的坑"卡片): + +- **最小 Qt 程序结构**:概念——`QApplication`是唯一的事件循环拥有者,`app.exec()`阻塞进入 + 事件循环分发系统事件;`Q_OBJECT`触发 moc 生成信号槽/属性系统所需的元对象代码;坑——一个 + 进程只能有一个 `QApplication`;忘记 `exec()` 窗口一闪而过;用了信号槽却忘记 `Q_OBJECT` + 会在链接阶段报 `undefined reference` 且报错不提示"缺宏"。 +- **控件创建与显示**:概念——QObject 对象树父子所有权模型,parent 销毁会自动 delete 所有 + 子对象;坑——忘记传 parent 造成真实内存泄漏;`move()` 手动定位的控件一旦套上布局会被覆盖。 +- **信号槽(内置/自定义/lambda)**:概念——观察者模式的 Qt 实现,发送者/接收者彻底解耦; + 新语法 `&Class::signal` 编译期类型检查,旧字符串语法 `SIGNAL()/SLOT()` 运行时检查;连接 + 方式分 Direct/Queued/Auto;坑——旧语法写错不报编译错误只是运行时静默连接失败;重载信号 + 不加 `QOverload` 消歧义会编译报错。 + **练(95min)**: - **任务卡 1 · 跑通最小程序** @@ -174,6 +224,18 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | 自定义控件与布局(`QHBoxLayout`) | `p03/ch08/custom_widget.cpp` | | **QSS 界面美化(教学补充)** | `docs/teaching/examples/qss_styling/qt_qss_styling.cpp` | +**核心概念与常见的坑**: + +- **资源系统 .qrc**:概念——`:/...` 路径是 Qt 资源系统,资源在编译期打包进可执行文件,运行时 + 不依赖外部文件;坑——`.qrc` 路径与代码引用不一致会运行时静默失败(不报编译错误);忘记接入 + CMake 的 `qt5_add_resources`/AUTORCC 资源系统不生效。 +- **布局管理(QHBoxLayout)**:概念——布局管理器接管子控件位置/大小计算,随窗口缩放自动重排; + 布局对象也遵循 QObject 父子所有权;坑——同一控件不能属于两个布局;布局设置后又用 + `setGeometry`/`move` 手动改位置会被下次刷新覆盖。 +- **QSS 界面美化**:概念——语法脱胎于 CSS(类型/ID 选择器、伪状态)+ 层叠继承规则,作用对象 + 是 QWidget 树而非 DOM;坑——ID 选择器必须先 `setObjectName`;`setStyleSheet` 会级联影响 + 所有子控件,范围失控是最容易踩的坑。 + **练(95min,计算器案例——练习任务,非预建方案)**: - **任务卡 1 · 计算器雏形** @@ -203,6 +265,18 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | 自定义事件处理 / 事件过滤器 | `p03/ch09/event_override_customwidget.cpp`、`event_filter_mainwindow.cpp` | | 鼠标事件 | `p03/ch09/mouse_event.cpp` | +**核心概念与常见的坑**(对应幻灯片 `day3.html`): + +- **Model/View 架构**:概念——数据(`QAbstractItemModel`)与展示(`QListView`/`QTableView`) + 彻底拆开,同一模型可喂给多个视图;`QModelIndex` + 角色(`Qt::DisplayRole`等)统一寻址; + 坑——改模型数据不发 `dataChanged`/`beginInsertRows` 信号,视图不会自动刷新。 +- **事件循环:事件 vs 信号**:概念——`app.exec()` 事件循环分发 `QEvent`(复用 Day2);事件是 + 框架推给控件的通知,信号槽是对象间主动通知,两者不同机制;坑——主线程做耗时操作会卡住整个 + 事件循环(呼应 Day4 QThread)。 +- **自定义事件处理 / 事件过滤器**:概念——两条拦截路径:重写 `xxxEvent()`(只影响自己)或 + `installEventFilter()`(统一拦截多个控件);事件有 `accept()`/`ignore()` 状态决定是否继续 + 传播;坑——重写事件函数忘记调用基类实现,会丢失控件默认行为。 + **练(95min)**: - **任务卡 1 · Model/View 换视图** @@ -224,6 +298,16 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | 结构化二进制读写 | `p03/ch11/qdatastream_io.cpp` | | **JSON 解析与写入(教学补充)** | `docs/teaching/examples/json_io/qt_json_parse_write.cpp` | +**核心概念与常见的坑**: + +- **QIODevice 统一读写抽象**:概念——`QFile`/`QTextStream`/`QDataStream` 都建立在 `QIODevice` + 统一接口上,之后遇到网络/串口等设备也是同一套接口;`QTextStream`面向文本行、`QDataStream` + 面向结构化二进制;坑——忘记检查 `open()` 返回值会静默失败;混淆文本/二进制模式会破坏内容。 +- **JSON 序列化/反序列化**:概念——序列化/反序列化是一对互逆操作,正确性标准是"写出去再读 + 回来数据一致"(round-trip);`QJsonValue` 是能装多种类型的变体;坑——不检查 + `QJsonParseError` 会让格式错误的输入静默变成空文档;类型不匹配 `toInt()`/`toString()` + 不报错只返回默认值。 + **练(95min,本地数据存储案例——练习任务,非预建方案)**: - **任务卡 1 · JSON 落盘** @@ -251,6 +335,16 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | 绘图设备(`QImage`/`QPixmap`/`QPicture`) | `p03/ch10/paint_device_qimage.cpp`、`paint_device_pixmaps.cpp`、`qpicture_io.cpp` | | **QThread 多线程(教学补充)** | `docs/teaching/examples/qthread_worker/qt_thread_worker.cpp` | +**核心概念与常见的坑**(对应幻灯片 `day4.html`): + +- **QPainter 绘图**:概念——Qt 绘制是框架驱动而非"想画就画",所有绘制只能发生在 + `paintEvent` 里,由 Qt 按需自动调用;主动触发重绘要用 `update()`(异步排队);坑——在 + `paintEvent` 之外直接画东西是未定义行为;数据变了忘记 `update()`界面不会自动重画。 +- **QThread 多线程**:概念——线程亲和性(每个 QObject 归属于创建/`moveToThread`到的线程, + GUI 控件必须归属主线程);官方推荐 worker-object 模式而非继承 QThread;跨线程信号槽自动 + 变 Queued,无需手动加锁;坑——在工作线程直接操作 GUI 控件是未定义行为/崩溃常见来源; + `moveToThread`要在`connect`和`start()`之前完成。 + **练(95min)**: - **任务卡 1 · 画一个简单图形界面** @@ -271,6 +365,17 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | **QPropertyAnimation 动画(教学补充)** | `docs/teaching/examples/property_animation/qt_property_animation.cpp` | | **QStateMachine 状态机(教学补充)** | `docs/teaching/examples/state_machine/qt_state_machine.cpp` | +**核心概念与常见的坑**: + +- **QPropertyAnimation 动画**:概念——动画建立在属性系统(`Q_PROPERTY`,同样靠 moc 生成) + 上,本质是"用缓动曲线在一段时间内不断调用属性的 setter",任何 `Q_PROPERTY` 都能被动画驱动; + 坑——属性必须是真正的 `Q_PROPERTY`,随手传错属性名不会报错只是什么也不发生;动画作用对象 + 在播放完成前被销毁会导致悬空访问。 +- **QStateMachine 状态机**:概念——让"状态"成为一等对象(`QState`),替代一堆 bool 标志位加 + if-else;状态转移(`addTransition`)本质是信号触发的转移,与信号槽自然衔接;坑——忘记 + `setInitialState`或`start()`,状态机对象存在但从未真正运行;状态未覆盖所有触发条件会导致 + "卡住"。 + **练(95min,交互式小游戏案例——练习任务,选做,非预建方案)**: - **任务卡 1 · 动画 + 状态机组合** @@ -299,6 +404,14 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | 标准化考核标准 | `docs/images/assessment_standard.png` | | 代码团队协同开发实操建议 | `docs/TRAINING_PLAN_2026.md` 「七、质量保证 (二) Git」 | +**核心概念与常见误区**(对应幻灯片 `day5.html`): + +- **Git 提交习惯**:概念——每完成一个任务都提交代码并写清任务描述,提交粒度对应 Jira 任务 + 粒度,commit 信息是可追溯记录;误区——攒几天代码一次性提交、commit 信息写"update",历史 + 记录失去可追溯性。 +- **标准化考核标准**:概念——项目管理评估(40%)+项目技术评估(30%)+职业素养评估(30%),技术 + 只占三成;误区——只顾埋头写代码、忽略项目管理和职业素养维度,另外 70% 权重被拉低。 + **讨论(95min)**: - **任务卡 1 · 分工复盘** @@ -315,6 +428,14 @@ Day2–5 讲:练 ≈ 1:2.7;Day1 模块 A 是讨论型任务(不追求"练" | Day2–4 案例回顾 | 请 1–2 组现场展示计算器/本地存储/动画状态机小游戏 | | OpenCV 与 AI 机器学习(可选,自学指针,无配套代码) | 方案书原文标注"可选择";如学员感兴趣,自学方向为 `cv::CascadeClassifier` 人脸检测入门,OpenCV 官方教程(需联网,选读) | +**核心概念与常见误区**: + +- **案例回顾**:概念——代码走查(code review)是团队协作常规环节,讲清"为什么这么设计"比 + "功能能跑"更能体现对 Day2–4 核心概念的理解;误区——只演示能跑不讲为什么,观摩组提不出有效 + 问题,复盘失去巩固概念的作用。 +- **OpenCV 与 AI(可选)**:概念——OpenCV 是独立于 Qt 的计算机视觉库,Qt 管界面/交互、 + OpenCV 管图像算法,两者通过 `cv::Mat` ↔ `QImage` 转换配合。 + **练(95min,综合演练)**: - **任务卡 1 · 三选一深化** diff --git a/docs/teaching/slides/custom.css b/docs/teaching/slides/custom.css index ef370de..4b30ac7 100644 --- a/docs/teaching/slides/custom.css +++ b/docs/teaching/slides/custom.css @@ -74,6 +74,35 @@ } .verify-box h4 { margin: 0 0 6px 0; color: #b7791f; } +/* 每个「讲」知识点的固定两件套:核心概念(是什么/为什么)+ 常见的坑(注意事项) */ +.concept-box { + border-left: 6px solid #2b6cb0; + background: #ebf4ff; + padding: 12px 22px; + margin: 10px 0; + border-radius: 4px; + text-align: left; + font-size: 0.58em; + line-height: 1.4; +} +.concept-box h4 { margin: 0 0 6px 0; color: #2b6cb0; } +.concept-box ul { margin: 4px 0 0 20px; padding: 0; } +.concept-box li { margin: 3px 0; } + +.pitfall-box { + border-left: 6px solid #c53030; + background: #fff5f5; + padding: 12px 22px; + margin: 10px 0; + border-radius: 4px; + text-align: left; + font-size: 0.58em; + line-height: 1.4; +} +.pitfall-box h4 { margin: 0 0 6px 0; color: #c53030; } +.pitfall-box ul { margin: 4px 0 0 20px; padding: 0; } +.pitfall-box li { margin: 3px 0; } + .reveal pre code { font-size: 0.68em; max-height: 480px; } .module-badge { diff --git a/docs/teaching/slides/day1.html b/docs/teaching/slides/day1.html index 2c2bf8c..80851eb 100644 --- a/docs/teaching/slides/day1.html +++ b/docs/teaching/slides/day1.html @@ -47,20 +47,52 @@

实习实训服务流程

- +

出处:docs/TRAINING_PLAN_2026.md - 「六、实习实训服务流程」——需求分析→需求评审→项目研发/代码实现→阶段评审→ - 项目测试/交付,这就是接下来 Day6–10 项目实作要走的完整流程。

+ href="../../TRAINING_PLAN_2026.md">docs/TRAINING_PLAN_2026.md 「六、实习实训服务流程」

+
+ +
+

服务流程:核心概念

+
+

核心概念

+ +
+
+

常见误区

+ +

质量保证:四平台一标准

- +

出处:docs/TRAINING_PLAN_2026.md - 「七、质量保证」——JIRA(任务跟踪)+ Git(版本管理)+ Wiki 知识库 + - OASIS 绿洲实训管理平台,是接下来项目实作阶段的四个协作工具。

+ href="../../TRAINING_PLAN_2026.md">docs/TRAINING_PLAN_2026.md 「七、质量保证」

+
+ +
+

四平台一标准:核心概念

+
+

核心概念

+ +
+
+

常见误区

+ +
@@ -112,8 +144,21 @@

namespace 作用域

-

"作用域"不是"文件顺序"——很多人误以为 namespace 只是给名字加前缀, - 实际上它定义的是一段可以被跨文件延续、跨块合并的作用域。

+
+

核心概念

+ +
+
+

常见的坑

+ +

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

@@ -121,16 +166,40 @@

函数重载决议

-

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

+
+

核心概念

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

常见的坑

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

出处:p01/ch03/function_overload_mechanism.cpp

引用的本质

-

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

+
+

核心概念

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

常见的坑

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

出处:p01/ch03/reference_essence.cpp

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

核心概念

+ +
+
+

常见的坑

+ +

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

模板与 STL 速览

-

只点名,不展开:泛型编程(函数模板/类模板)+ 容器/迭代器,是 Day6–10 - 项目实作里会大量用到的基础设施。

+
+

核心概念

+ +
+
+

常见的坑

+ +

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

@@ -158,19 +254,47 @@ delete p; // 如果 ~Base() 不是 virtual,~Derived() 不会被调用——

CMake:用本仓库当教材

cmake -S . -B build -G Ninja -DBUILD_QT_PART=OFF -DCMAKE_BUILD_TYPE=Debug
 cmake --build build   # 144 个目标全绿
-

出处:仓库根 CMakeLists.txt—— - option(BUILD_QT_PART) 控制是否编译 Qt 部分,每个示例一个 - add_executable 目标,这就是"一文件一目标"约定的由来。

+
+

核心概念

+ +
+
+

常见的坑

+ +
+

出处:仓库根 CMakeLists.txt

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

-

C++ 生态的包管理器,解决"第三方库怎么装、怎么被 CMake 找到"的问题。

+
+

核心概念

+ +
+
+

常见的坑

+ +

缺口说明

-

本仓库用的是"隔离 Qt 手动装"(tools/install_qt_host.sh) - 而非 vcpkg,两种思路都要认识;本仓库暂无 vcpkg 可运行示例——安装真实 - vcpkg 工具链超出最小示例范畴,此处仅作概念介绍。

+

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

diff --git a/docs/teaching/slides/day2.html b/docs/teaching/slides/day2.html index 0b126dc..2ade562 100644 --- a/docs/teaching/slides/day2.html +++ b/docs/teaching/slides/day2.html @@ -42,6 +42,30 @@ cmake --build build +
+

最小 Qt 程序:核心概念

+
+

核心概念

+ +
+
+

常见的坑

+ +
+
+

最小 Qt 程序:QApplication + exec()


@@ -63,6 +87,28 @@ int main(int argc, char* argv[]) {
     QTimer::singleShot 200ms 自动退出,便于离屏自动化验证,真实项目不需要这一行)

+
+

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

+
+

核心概念

+
    +
  • QObject 有一套"对象树"父子所有权模型:给控件传入 parent 后, + parent 被销毁时会自动 delete 所有子对象——这就是为什么 Qt 代码里到处是 + new 却几乎不见 delete
  • +
  • 两种写法(先构造后设属性 / 构造时一次性传参)本质相同,父对象的作用一样。
  • +
+
+
+

常见的坑

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

控件创建:两种写法


@@ -79,6 +125,32 @@ QPushButton* btn2 = new QPushButton(QStringLiteral("孙悟空"), this);
     href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.cpp

+
+

信号槽:观察者模式 + moc

+
+

核心概念

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

常见的坑

+
    +
  • 旧的字符串语法 SIGNAL(clicked()) 函数名/参数类型写错不会编译报错, + 只会运行时"连接失败"且经常没有提示——能用新语法就不用旧语法,编译期就能发现问题。
  • +
  • 重载信号(如右侧例子的两个 hungry())不加 QOverload + 消歧义,编译器不知道该连哪个重载,直接报错。
  • +
+
+
+

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


@@ -160,6 +232,27 @@ connect(t, QOverload<QString>::of(&Teacher::hungry), s, QOverload<QStri
     

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

+
+

资源系统:.qrc 是什么

+
+

核心概念

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

常见的坑

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

常用控件:QLabel 显示图片


@@ -173,6 +266,28 @@ label->setPixmap(pixmap);
     HTML 富文本标签见 label_text_html.cpp

+
+

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

+
+

核心概念

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

常见的坑

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

自定义控件与布局:QHBoxLayout


@@ -190,6 +305,29 @@ connect(slider, &QSlider::valueChanged, spin, &QSpinBox::setValue);
     不用手算控件坐标。

+
+

QSS:类 CSS 的样式层叠

+
+

核心概念

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

常见的坑

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

QSS 界面美化(教学补充)


diff --git a/docs/teaching/slides/day3.html b/docs/teaching/slides/day3.html
index 4dedee7..1199861 100644
--- a/docs/teaching/slides/day3.html
+++ b/docs/teaching/slides/day3.html
@@ -30,6 +30,30 @@
     

Model/View 架构 / 事件机制

+
+

Model/View:数据与展示分离

+
+

核心概念

+
    +
  • Model/View 把"数据"和"怎么展示数据"彻底拆开:模型(QAbstractItemModel + 及其子类)只负责存数据、回答"这个位置是什么",视图(QListView/ + QTableView/...)只负责画——同一份模型可以同时喂给多个不同视图。
  • +
  • 模型用统一的 QModelIndex(行/列/父项)寻址数据,视图向模型要数据时 + 还要带上"角色"(Qt::DisplayRole 显示文本、Qt::DecorationRole + 图标……),同一个位置可以按不同角色返回不同内容。
  • +
+
+
+

常见的坑

+
    +
  • 直接改模型内部数据却不发信号(dataChanged/beginInsertRows+ + endInsertRows),视图不会自动刷新,界面和数据"对不上"。
  • +
  • 用了简单的 QStandardItemModel 却又想自定义复杂数据结构:应该继承 + QAbstractItemModel 自己实现,而不是硬塞进 Item 模型。
  • +
+
+
+

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


@@ -50,6 +74,31 @@ connect(view->selectionModel(), &QItemSelectionModel::currentChanged, ...);
     仓库 p03/ 的 wiki 抓取内容没有 Model/View,这是本轮补写的教学补充示例,已离屏验证跑通。

+
+

事件循环:事件 vs 信号

+
+

核心概念

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

常见的坑

+
    +
  • 在事件循环所在的主线程做耗时操作会直接卡住整个界面(无法重绘、按钮点了没反应)—— + 这也是 Day4 要讲 QThread 的原因。
  • +
+
+
+

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

app.exec() 进入的就是事件循环——鼠标点击、键盘输入、定时器 @@ -59,6 +108,29 @@ connect(view->selectionModel(), &QItemSelectionModel::currentChanged, ...); href="../../../p03/ch02/minimal_qt_app.cpp">p03/ch02/minimal_qt_app.cpp

+
+

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

+
+

核心概念

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

常见的坑

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

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


@@ -123,6 +195,29 @@ protected:
     

文件 IO / JSON 解析

+
+

QIODevice:统一的读写抽象

+
+

核心概念

+
    +
  • QFile/QTextStream/QDataStream 都建立在 + QIODevice 这一统一读写接口上——同样的"打开/读/写/关闭"套路,以后遇到网络 + 套接字/串口一类设备也是同一套接口,学一次可以迁移到别的设备类型。
  • +
  • QTextStream 面向"文本行"(人可读),QDataStream 面向 + "结构化二进制"(保留类型信息,程序间高效互传但人不可直接读)——按需求选,不是谁更好。
  • +
+
+
+

常见的坑

+
    +
  • 忘记检查 open() 的返回值:文件不存在/无权限时静默失败,后续读写在一个 + 没打开的设备上操作,容易读出空数据而不报错。
  • +
  • 混淆文本模式和二进制模式(QIODevice::Text):文本模式下 Qt 会做换行符 + 转换,用来读写二进制数据会破坏内容。
  • +
+
+
+

文件系统:QFile 读文本


@@ -136,6 +231,29 @@ while (!file.atEnd()) { qDebug() << file.readLine(); }
     qdatastream_io.cpp

+
+

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

+
+

核心概念

+
    +
  • JSON 读写的本质是"内存对象 ↔ 文本"的一对互逆操作:序列化(QJsonDocument::toJson) + 把内存数据变成可存/可传的文本,反序列化(fromJson)再变回来——正确性的标准是 + "写出去再读回来,数据要一致"(round-trip)。
  • +
  • QJsonValue 是一个变体(variant)类型,能装字符串/数字/布尔/数组/对象, + 取值时要按预期类型调用 toString()/toInt() 等。
  • +
+
+
+

常见的坑

+
    +
  • 忘记检查 QJsonDocument::fromJsonQJsonParseError: + 文本格式错误时返回一个空文档,后续取值全是默认值,不会崩溃也不会报错,很难定位。
  • +
  • 类型不匹配时 toInt()/toString() 等不会抛异常,只会静默 + 返回默认值(0/空串),排查时容易误以为"数据没写进去"。
  • +
+
+
+

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


diff --git a/docs/teaching/slides/day4.html b/docs/teaching/slides/day4.html
index f47276e..eb6c992 100644
--- a/docs/teaching/slides/day4.html
+++ b/docs/teaching/slides/day4.html
@@ -30,6 +30,30 @@
     

2D 绘图 / 多线程

+
+

QPainter:由 Qt 驱动的重绘模型

+
+

核心概念

+
    +
  • Qt 的绘制是"框架驱动"而非"你想画就画":所有绘制只能发生在 paintEvent + 里,由 Qt 在需要重绘时(窗口刚显示、被遮挡后重新露出、你主动请求)自动调用——这和直接式 + 绘图(想画就立刻画到屏幕)是不同的模型。
  • +
  • 控件想主动触发重绘要调用 update()(异步,排队到下次事件循环处理)而不是 + 直接调用 paintEvent()QPainter 只是"在哪个设备上画"的画笔, + 画布可以是控件本身,也可以是 QImage/QPixmap 等离屏设备。
  • +
+
+
+

常见的坑

+
    +
  • paintEvent 之外的地方直接 new QPainter(this) 画东西: + 要么无效要么未定义行为,绘制必须在 paintEvent 回调期间进行。
  • +
  • 数据变了却忘记调用 update():界面不会自动感知"要重画了",看起来像是 + "画错了",其实是"根本没重画"。
  • +
+
+
+

绘图:QPainter


@@ -48,6 +72,33 @@ void paintEvent(QPaintEvent*) override {
     paint_device_pixmaps.cpp

+
+

QThread:线程亲和性与 worker-object

+
+

核心概念

+
    +
  • "线程亲和性"(thread affinity):每个 QObject 都归属于创建它/被 + moveToThread 移动到的那个线程,它的槽函数默认在所属线程执行——GUI 控件必须 + 始终归属主线程,这是 Qt 线程模型的核心约束。
  • +
  • 官方推荐的是 worker-object 模式(把耗时逻辑放进一个普通 QObject, + moveToThread 到工作线程),而不是继承 QThread 重写 + run()——前者能继续用信号槽通信,后者容易把线程亲和性搞混。
  • +
  • 跨线程的信号槽连接会自动变成 Queued(回顾 + Day2),结果通过事件循环 + 安全地"带回"接收者所在线程,不需要手动加锁。
  • +
+
+
+

常见的坑

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

QThread 多线程(教学补充)


@@ -117,6 +168,32 @@ connect(&workerThread, &QThread::started, &worker, &Worker::process);
     

动画框架 / 状态机

+
+

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

+
+

核心概念

+
    +
  • Qt 的属性系统(Q_PROPERTY,同样靠 + Day2 讲过的 moc 生成)给每个属性配了 + 统一的 get/set 接口;动画框架的本质就是"在一段时间内,用缓动曲线不断调用某个属性的 + setter",所以任何声明成 Q_PROPERTY 的属性(不只是内置的 geometry/opacity) + 都能被动画驱动。
  • +
  • QEasingCurve 决定的是"数值随时间变化的曲线形状"(线性/回弹/加减速……), + 动画本身只是"定时器 + 插值 + 调 setter"的组合,理解这点能解释为什么几乎所有属性都能做动画。
  • +
+
+
+

常见的坑

+
    +
  • 要动画化的属性必须是真正的 Q_PROPERTY(有 setter),随手传一个字符串 + 属性名如果对应类没有声明该属性,动画不会报错,只是什么也不会发生。
  • +
  • 动画作用的对象在动画播放完成前被销毁会导致悬空访问; + QAbstractAnimation::DeleteWhenStopped 能让动画结束后自动释放自身,但不管 + 目标对象的生命周期,目标对象仍需自己保证不提前销毁。
  • +
+
+
+

QPropertyAnimation 动画(教学补充)


@@ -132,6 +209,31 @@ anim->start(QAbstractAnimation::DeleteWhenStopped);
     (配套 README

+
+

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

+
+

核心概念

+
    +
  • 状态机解决的是"同一个控件在不同状态下行为/外观不同"的问题——与其用一堆 + bool 标志位加 if-else 判断当前处于什么状态,不如让"状态"本身成为一等对象 + (QState),每个状态自己声明"进入这个状态要怎样"(assignProperty, + 又用到了 Qt 属性系统)。
  • +
  • 状态之间的切换(addTransition)本质是"信号触发的转移"——转移条件就是 + 一个信号,这让状态机和信号槽自然衔接:任何能发信号的事件都能驱动状态切换。
  • +
+
+
+

常见的坑

+
    +
  • 忘记 machine.setInitialState(...) 或忘记调用 machine.start(): + 状态机对象存在但从未真正运行,所有 assignProperty/addTransition + 都不会生效。
  • +
  • 某个状态没有覆盖所有可能触发的信号/条件,会导致状态机"卡住"——设计状态机前先把所有 + 状态和转移条件画成图,比直接写代码更不容易漏。
  • +
+
+
+

QStateMachine 状态机(教学补充)


diff --git a/docs/teaching/slides/day5.html b/docs/teaching/slides/day5.html
index 7c333dc..1eddc8f 100644
--- a/docs/teaching/slides/day5.html
+++ b/docs/teaching/slides/day5.html
@@ -34,20 +34,52 @@
 
   

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

- +

出处:docs/TRAINING_PLAN_2026.md - 「七、质量保证 (二) Git」——接下来 Day6–10 项目实作,每人每完成一个任务都要提交代码 - 到 Git 并写清任务描述。

+ href="../../TRAINING_PLAN_2026.md">docs/TRAINING_PLAN_2026.md 「七、质量保证 (二) Git」

+
+ +
+

Git 提交习惯:核心概念

+
+

核心概念

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

常见误区

+
    +
  • 攒了几天代码一次性提交、commit 信息写"update":历史记录失去可追溯性,出问题时无法 + 定位是哪次改动引入的。
  • +
+

标准化考核标准

- +

出处:docs/TRAINING_PLAN_2026.md - 「七、(五) 标准化的考核标准」——项目管理评估(40%) + 项目技术评估(30%) + - 职业素养评估(30%),Day6–10 项目实作最终按这个标准打分。

+ href="../../TRAINING_PLAN_2026.md">docs/TRAINING_PLAN_2026.md 「七、(五) 标准化的考核标准」

+
+ +
+

考核标准:核心概念

+
+

核心概念

+
    +
  • 项目管理评估(40%) + 项目技术评估(30%) + 职业素养评估(30%)——技术实现只占三成, + Day6–10 项目实作最终按这三个维度综合打分,不是"代码能跑就满分"。
  • +
+
+
+

常见误区

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

Day2–4 案例回顾

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

+
+

核心概念

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

常见误区

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

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

+
+

核心概念

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

缺口说明

方案书原文对本项标注"可选择",属于非必修拓展。引入 OpenCV 会新增一条完整的