PPT 版本幻灯片:生成器 + 5 天讲稿备注(已去 AI 味)
- tools/teaching_ppt/gen.py:python-pptx 生成引擎(页面内容与讲稿数据分离, 幂等可重生成);tools/teaching_ppt/day1..5.py:每天的页面+讲稿数据 - docs/teaching/ppt/day1..5.pptx:5 份 PPT 产物,页面内容对应 slides/*.html, 详细讲稿(建议时长/讲法/演示/互动/过渡)只写入演讲者备注,不进主页面 - 讲稿去 AI 味:破折号过度堆砌 152 处砍到 6 处(7.8‰ → 0.3‰)、 升华腔(分水岭/点燃探索欲)清零、重复词(现场 26→11)替换; 保留教学口语生动感(你们/当堂/点破/埋点/回指)与【过渡】悬念钩子 - 生成方式:python-pptx 在隔离 venv 装,不影响系统 Python; 改 day*.py 的 notes 字段后重跑 gen.py 即可幂等重生成
This commit is contained in:
@@ -0,0 +1,310 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Day1 · 开训 + C++ 回顾与提升(全天 C++)—— 页面对应 slides/day1.html。"""
|
||||
|
||||
SLIDES = [
|
||||
{
|
||||
'kind': 'title',
|
||||
'badge': 'C++/Qt 方向实训 · 第 1 / 4 个讲授日 · 对齐 2026-07 新版 10 天安排',
|
||||
'title': 'Day 1 · 开训 + C++ 回顾与提升',
|
||||
'subtitle': '全天 C++:技术版图 / 环境与 CMake / OOP / 继承多态 / 内存管理 / STL',
|
||||
'bullets': [
|
||||
'上午 08:30–11:00 讲授:开训 · C++ 技术版图 · 环境与 CMake · PRETEST 摸底 · OOP 回顾',
|
||||
'上午 11:00–12:00 课堂练习:独立完成 Part1 构建(144 目标全绿)',
|
||||
'下午 13:30–16:00 讲授:继承与多态 · 内存管理与所有权 · STL · 现代 C++ 巡礼',
|
||||
'下午 16:00–17:00 作业:ERRATA 修坑讲解 + 版本探针(选做)',
|
||||
],
|
||||
'source': '大纲:docs/teaching/OUTLINE_4DAY.md · 摸底卷:docs/teaching/PRETEST.md',
|
||||
'minutes': 3, 'segment': '开场',
|
||||
'notes': """【定位】今天全天 C++,是"回顾和提升",不是零基础教学。开场就把这话挑明:基础好的别嫌无聊,基础弱的别慌,今天讲的是"学过但容易讲不透"的那些点。
|
||||
【时长骨架】上午:开训典礼约 40 分钟(组织方)→ 技术版图讲座 25 → 环境与 CMake 25 → 编译vs链接 10 → PRETEST 摸底 10 → OOP 回顾 25,留 15 分钟机动。下午:讲授 6 页约 90 分钟,穿插 3 次 15 分钟随堂练,再留 15 分钟机动。
|
||||
【讲法】本页只念四行安排,强调两点:每半天都有动手环节;今天的产出(构建全绿)是后面四天所有演示的前提。"""
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': 'C++ 常用技术和框架(讲座)',
|
||||
'boxes': [
|
||||
('concept', '核心概念——C++ 的四大应用版图', [
|
||||
'桌面客户端:Qt(本次实训主线)、跨平台 GUI',
|
||||
'服务端/基础设施:高性能网络服务、数据库引擎、游戏服务器',
|
||||
'嵌入式/车载:资源受限环境的第一梯队语言',
|
||||
'游戏/图形:UE 引擎、渲染管线',
|
||||
]),
|
||||
('info', '而且它还在进化', [
|
||||
'C++11 起三年一版:11「几乎新语言」→ 17 实用大件 → 20 四大件',
|
||||
'自学入口:docs/teaching/MODERN_CPP.md · MODERN_CPP_20PLUS.md(今天下午末尾回收)',
|
||||
]),
|
||||
],
|
||||
'minutes': 25, 'segment': '上午 · 讲座(开训典礼后)',
|
||||
'notes': """【开场】开训典礼刚结束,注意力还散着。用一个提问把它接回来:"你们手机和电脑里,哪些软件是 C++ 写的?"微信桌面版、Chrome 内核、各种游戏引擎,基本都躲不开。
|
||||
【讲法】四大版图每个讲 4–5 分钟:举一个具体产品,再说清楚这个领域为什么离不开 C++(性能可控、贴近硬件、生态成熟)。讲到桌面版图时落到 Qt,预告一句:"明天开始四天全是它。"
|
||||
【第二个盒子】只花 3 分钟。告诉学员 C++ 不是老古董,三年一版在进化;两份仓库内文档是自学材料,下午末尾还会回来点一次,现在不展开。
|
||||
【过渡】"要用这门语言干活,先得把工程环境立起来。下一页,构建工具。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': 'CMake:用本仓库当教材',
|
||||
'code': 'cmake -S . -B build -G Ninja -DBUILD_QT_PART=OFF -DCMAKE_BUILD_TYPE=Debug\ncmake --build build # 144 个目标全绿',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'CMake 是「生成构建系统的工具」不是构建系统本身:-S -B 生成 Ninja/Makefile 工程,--build 才是真正编译',
|
||||
'Makefile 是 CMake 的生成目标之一——这就是两者的关系',
|
||||
'option(BUILD_QT_PART) 控制是否编 Qt 部分;每个示例一个 add_executable 目标',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'换生成器/清缓存后要重新走一次 -S -B(大多数普通改动 CMake 会自动侦测)',
|
||||
]),
|
||||
('info', '包管理器一分钟', [
|
||||
'vcpkg:装库 + toolchain 文件 → find_package 自动定位;本仓库选「隔离 Qt 手动装」——两种思路都要认识',
|
||||
]),
|
||||
],
|
||||
'source': '出处:仓库根 CMakeLists.txt',
|
||||
'minutes': 25, 'segment': '上午 · 环境与构建',
|
||||
'notes': """【讲法】本页是演示页,不是念 PPT:切到终端,真敲上面两条命令(预先建过一次 build,第二次跑很快),学员就能看到 144 个目标滚屏全绿。
|
||||
【关键区分】"生成"与"构建"两阶段用装修类比:CMakeLists 是图纸,-S -B 是出施工方案(Ninja 文件),--build 才是开工。学员以后 90% 的 CMake 困惑,都出在分不清这两步。
|
||||
【Makefile 的处理】新安排点名了 Makefile,这里用一句话安放它:打开 build/ 目录看生成产物,告诉学员"你们以前手写的 Makefile,在 CMake 工作流里是被生成出来的",不单独展开手写语法。
|
||||
【包管理器】最后 2 分钟带过 vcpkg 概念即可。本仓库用隔离安装,明天装 Qt 时会亲眼看到。
|
||||
【过渡】"构建会失败,失败分两种。下一页教你一眼分辨。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': '★ 编译错误 vs 链接错误:排查方向完全不同',
|
||||
'code': "main.cpp:12:5: error: 'foo' was not declared <- 编译期:带文件行号\nmain.o: undefined reference to `Widget::bar()' <- 链接期:没有行号",
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'error: 带行号 = 编译期(语法/类型/没声明)——回代码查那一行',
|
||||
'undefined reference 无行号 = 链接期(有声明没定义/缺库/缺目标文件)——查「定义在哪、链没链进来」',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'见到 undefined reference 就回头查语法拼写——方向错了',
|
||||
'伏笔:明天 Qt 忘写 Q_OBJECT 报的正是链接错误,且报错完全不提宏',
|
||||
]),
|
||||
],
|
||||
'minutes': 10, 'segment': '上午 · 环境与构建',
|
||||
'notes': """【讲法】把上面两行报错投在屏幕上,让学员找不同:一个有 main.cpp:12:5,一个只有 main.o。就用"有没有行号"这个最粗暴的判据,帮他们建立第一反应。
|
||||
【为什么现在讲】这不是语法点,是排错元技能。今天下午写练习、明天起写 Qt,报错会立刻大量出现,先给分类器再给知识。
|
||||
【伏笔要点破】明确预告:明天会有一个"忘写宏导致链接错误"的经典坑,报错信息完全不提宏。到时候谁先想起今天这页,谁先解决。
|
||||
【过渡】"讲了半天,你们的底子到底怎么样?测一下,10 分钟。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 摸底 10 分钟',
|
||||
'title': 'PRETEST:10 题摸底(闭卷、独立完成)',
|
||||
'bullets': [
|
||||
'双平行卷 A/B 交错发放,防抄袭;语言基线 C++98,只测经典功底',
|
||||
'三级梯度:L1 基础语法(1–3 题)/ L2 OOP 与内存(4–7 题)/ L3 多态细节(8–10 题)',
|
||||
],
|
||||
'boxes': [
|
||||
('info', '测完怎么用', [
|
||||
'L1 大面积错 → 回退预案(放慢 OOP 回顾、下午 STL 让位基础加固)',
|
||||
'L2 部分错 → 对应知识点讲授时加时;L3 全对 → 全速推进',
|
||||
]),
|
||||
],
|
||||
'source': '卷面与分层预案:docs/teaching/PRETEST.md',
|
||||
'minutes': 10, 'segment': '上午 · 摸底',
|
||||
'notes': """【操作】A/B 卷按座位交错发。规则宣布清楚:闭卷、不查资料、10 分钟,答不出就空着。这是给教学定速用的,不计成绩(这句必须说,否则有人抄有人慌)。
|
||||
【计时】投屏倒计时 10 分钟。期间不讲课,巡场看答题情况,L1 三道题扫一眼就能看出班级底色。
|
||||
【收卷后】当场只判 L1(30 秒能判完一摞)。L1 通过率高就按原速走;低于一半,启用 PRETEST 里的回退预案:下午 STL 压缩为 10 分钟点名,把时间还给继承与多态。L2/L3 课后判,用于明天微调。
|
||||
【过渡】"卷子里的问题,接下来两个小时会逐个讲透。先从最基本的:类和封装。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': 'OOP 回顾:类 / 对象 / 封装 + namespace',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'封装 = 数据和操作绑定 + 访问控制(public/private 是给「人」划的边界,不是安全机制)',
|
||||
'构造/析构对应对象生命周期的两端——这个模型下午讲「所有权」还要用',
|
||||
'namespace 的「作用域」不是「文件顺序」:可跨文件延续、跨块合并',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'头文件里 using namespace std; 会把污染带给所有 include 它的文件——只在 .cpp 用,或全程写限定名',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p01/ch04/(类与对象各节)· p01/ch03/namespace_syntax.cpp · 对照 docs/ERRATA.md',
|
||||
'minutes': 25, 'segment': '上午 · OOP 回顾(按 PRETEST 结果调速)',
|
||||
'notes': """【调速点】这一页的深度看刚才 L1 判卷结果:底子好,10 分钟把三个概念点过,多出的时间给学员提问;底子弱,打开 p01/ch04 的源文件逐个走读,讲满 25 分钟。
|
||||
【讲法】封装讲"边界":private 挡的是队友的手滑,不是黑客,工程含义大于语言含义。构造/析构用"出生登记/注销"类比,并预告:下午所有权、明天 Qt 父子树,全在这条线上。
|
||||
【namespace】重点讲 ERRATA 里的真实修正案例(wiki 原文把作用域理解成文件顺序)。打开 docs/ERRATA.md 对应条目,让学员看到"教材也会错,错误可以被记录和修正"。这也是下午作业的预演。
|
||||
【过渡】"上午最后一小时,轮到你们上手。任务卡在下一页。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 课堂练习 11:00–12:00(工程师指导、答疑)',
|
||||
'title': '任务卡 1 · 走一遍真实构建流程',
|
||||
'boxes': [
|
||||
('task', '任务卡 1', [
|
||||
'目标:独立完成本仓库 Part1 的配置+构建,不看着教师操作',
|
||||
'验收:cmake -S . -B build -G Ninja -DBUILD_QT_PART=OFF 配置成功;cmake --build build 144 目标全绿',
|
||||
'验收:能说出 option(BUILD_QT_PART) 的作用',
|
||||
'参考:仓库根 CMakeLists.txt · CMake 官方 Tutorial(选读·需联网)',
|
||||
]),
|
||||
],
|
||||
'minutes': 60, 'segment': '上午 · 课堂练习',
|
||||
'notes': """【组织】学员独立操作,教师和助教巡场。预期 20 分钟内多数人完成两条命令;剩下时间处理个体环境问题(权限、Ninja 没装、路径带中文空格等,这些"环境坑"本身就是教学素材,遇到就广播讲)。
|
||||
【常见卡点】①在错误目录下执行(顺势回收上午"工作目录"意识,Day4 还会正式讲);②build 目录残留旧缓存报错,正好演示"清缓存要重新 -S -B"。
|
||||
【提前完成者】引导做两件事:给 BUILD_QT_PART=ON 试一次(会因没装 Qt 停在配置期,预告明天装);或提前看 docs/ERRATA.md 挑下午作业要讲的条目。
|
||||
【收尾 5 分钟】随机抽两人口头回答 option(BUILD_QT_PART) 的作用,达标才算过验收。"""
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': '下午总览:继承多态 / 内存管理 / STL',
|
||||
'bullets': [
|
||||
'C++ 核心特性:函数重载决议 · 虚函数与虚析构',
|
||||
'内存管理:引用的本质 · ★所有权——谁负责 delete',
|
||||
'STL 常用容器 + 迭代器',
|
||||
'收尾:现代 C++ 巡礼(兴趣钩子)',
|
||||
],
|
||||
'minutes': 3, 'segment': '下午 · 开场',
|
||||
'notes': """【节奏】下午 6 个知识点约 90 分钟讲授,每讲完 2 个插一次 10–15 分钟随堂练(写几行代码验证刚讲的行为),最后留 15 分钟机动。
|
||||
【主线】向学员挑明下午的暗线:所有内容都在回答"对象什么时候生、什么时候死、死的时候发生什么"。这条线明天会直接接到 Qt 的对象树上。"""
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': '函数重载决议',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'编译器靠名字改写(name mangling)把重载变成不同符号',
|
||||
'二义性往往不是写错,而是候选集中有两个「一样好」的候选',
|
||||
'重载(编译期·同类内)vs 重写(运行期·继承体系)是两码事',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'隐式类型转换会扩大候选集——传 long 可能触发意料之外的重载版本',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p01/ch03/function_overload_mechanism.cpp',
|
||||
'minutes': 15, 'segment': '下午 · 继承与多态',
|
||||
'notes': """【引入】把 PRETEST 第 4 题(h(long)/h(double) 传 5)投出来重做一遍。这题当堂错误率通常最高,正好当引子。
|
||||
【讲法】用 nm 或编译器输出展示一次名字改写后的符号(_Z1hl 之类),学员对"重载在符号层怎么实现"会豁然开朗,顺带把上午"链接错误查符号"的意识再钉一次。
|
||||
【随堂练点】讲完本页加下一页后插第一次随堂练(15 分钟):让学员亲手制造一个二义性调用并读懂报错。
|
||||
【过渡】"重载是编译期挑函数;运行期挑函数就是多态。它有个最容易漏的角落:析构。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': '虚函数与虚析构',
|
||||
'code': 'Base* p = new Derived();\ndelete p; // 如果 ~Base() 不是 virtual,~Derived() 不会被调用——经典泄漏',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'经基类指针 delete 派生对象时,只有基类析构是 virtual 才会沿虚表调到派生类析构',
|
||||
'这就是「多态」在析构场景下的具体体现',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'类可能被继承 + 可能经基类指针删除 ⇒ 析构就该 virtual(防御性约定)',
|
||||
'明天会看到 Qt 的 QObject 体系全线虚析构',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p01/ch04/s08/virtual_dtor_purpose.cpp',
|
||||
'minutes': 18, 'segment': '下午 · 继承与多态',
|
||||
'notes': """【讲法】当堂跑 p01/ch04/s08/virtual_dtor_purpose.cpp(tools/run.sh),先注释掉 virtual 看派生类析构不打印,再加回去看两级析构依次打印。"看见泄漏"比讲十分钟原理有效。
|
||||
【原理层】底子好的班补讲虚表指针:对象头部藏着 vptr,delete 时经 vptr 找到"真正的类型"的析构。底子弱的班跳过 vptr,只记防御性约定。
|
||||
【互动】提问:"STL 容器(vector)为什么没有虚析构却没事?"因为没人拿基类指针删它,规则是条件式的,不是"所有类都要 virtual"。
|
||||
【随堂练】此处插第一次随堂练 15 分钟:改 virtual_dtor_purpose.cpp 自己复现一次泄漏与修复。"""
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': '引用的本质',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'把引用理解成「自动解引用的 const 指针」——能解释为什么必须初始化、不能重新绑定',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'返回局部变量的引用:函数结束即悬空,访问是未定义行为——和返回局部指针同一类坑',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p01/ch03/reference_essence.cpp',
|
||||
'minutes': 12, 'segment': '下午 · 内存管理',
|
||||
'notes': """【引入】PRETEST 第 1 题(引用能否绑 nullptr)回收:用"const 指针"视角一句话解释所有选项。
|
||||
【讲法】画两张内存图:指针是"另一个格子存地址",引用是"同一个格子的别名"。但底层实现常常就是指针,语言只是不让你碰它。
|
||||
【悬空引用】强调它是"时间维度"的错误:代码每一行单看都对,错在对象死了引用还活着。这正是下页"所有权"和 Day2 lambda 悬空坑的同一个病根,讲到那里会反复回指这页。
|
||||
【过渡】"对象死了还被人用。那到底该谁管对象的死活?下一页,全天最重要的一页。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': '★ 所有权:谁负责 delete?',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'内存管理的核心问题不是「会不会 new」,而是「这块资源归谁管、由谁释放」',
|
||||
'现代答案:RAII / 智能指针——unique_ptr 独占、shared_ptr 共享(MODERN_CPP.md §2)',
|
||||
'一个对象只能有一个所有者',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'双重释放 / 忘记释放 / 释放后使用——三兄弟全部源于所有权不明',
|
||||
'明天的伏笔:Qt 用「父子树」回答同一个问题,两套所有权体系不能混用',
|
||||
]),
|
||||
],
|
||||
'minutes': 18, 'segment': '下午 · 内存管理',
|
||||
'notes': """【地位】如果今天只能让学员带走一页,就是这页。所有权心智模型是"学过 C++ 语法"和"写得出不崩的程序"之间最大的那道坎,Day2 的三个崩溃坑全是它的变体。
|
||||
【讲法】先讲问题再讲方案:举一个函数返回 new 出来的指针的例子,问"谁该 delete?调用方?被调方?文档里写了吗?"让学员体会到,"所有权不明"本身就是 bug 温床。然后给出 unique_ptr(独占,move 转移)和 shared_ptr(引用计数),作为把所有权写进类型的方案。
|
||||
【尺度】智能指针只讲到"是什么、解决什么",用法细节指向 MODERN_CPP.md §2 自学。因为明天起 Qt 的世界用父子树而不是智能指针,别让学员在 Qt 代码里到处套 shared_ptr(这是真实的常见误用)。
|
||||
【伏笔】用加重语气念出"两套所有权体系不能混用",预告明天上午有一个现场崩溃演示。
|
||||
【随堂练】此处插第二次随堂练 15 分钟:给一段双重释放的代码,让学员用"所有权归谁"的语言说出错因。"""
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': '模板与 STL 常用容器',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'泛型编程:同一份代码适配不同类型;vector / map / string 是项目实作三件套',
|
||||
'迭代器是「容器和算法之间的统一接口」——STL 算法能脱离具体容器复用的关键',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'容器扩容/删元素后旧迭代器可能失效(如 vector 搬家),继续用是未定义行为',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p01/ch05/class_template_basics.cpp · p01/ch09/s03/vector_iterator.cpp',
|
||||
'minutes': 15, 'segment': '下午 · STL',
|
||||
'notes': """【调速点】PRETEST 弱班把本页压到 8 分钟,只讲 vector 加失效坑;强班讲满,并当堂演示一次 push_back 导致迭代器失效的未定义行为(可能崩也可能"正常",顺势又强化一次"UB 不一定崩")。
|
||||
【讲法】三件套各给一个"项目里会怎么用"的场景:vector 存记录列表(Day5 案例真的用)、map 做查找表、string 到处都是。迭代器讲成"统一的游标协议",range-for 是它的糖。
|
||||
【失效坑】口诀给到位:"遍历中增删,先查该容器的失效规则。"不要求背规则,要求知道有这回事、会去查。
|
||||
【随堂练】第三次随堂练 15 分钟:写 10 行代码,vector 边遍历边 erase,观察行为并修复(用 erase 返回值)。"""
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00 · 兴趣钩子',
|
||||
'title': 'C++11/14/17/20+:一门仍在进化的语言',
|
||||
'boxes': [
|
||||
('concept', '每 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 以来最大变革」',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'新标准 ≠ 现在能用:生产工具链常年落后 1–2 版;用 tools/std_probe.py 实测(宿主 gcc 13.3 到 C++23)',
|
||||
]),
|
||||
],
|
||||
'source': '自学:docs/teaching/MODERN_CPP.md(11/14/17,仓库基线可编译)· MODERN_CPP_20PLUS.md(20/23/26 + GCC 实测矩阵)',
|
||||
'minutes': 12, 'segment': '下午 · 收尾钩子',
|
||||
'notes': """【定位】这页是点火不是授课,目标是让学有余力的人课后打开那两份文档,不是当堂教会任何新特性。
|
||||
【讲法】每个版本挑一个"最有体感"的特性各 1–2 分钟:C++11 讲 lambda(明天信号槽就大量用,这个衔接要点破);C++17 讲结构化绑定的赏心悦目;C++20 讲 Concepts 把几百行模板报错变一句人话。
|
||||
【演示(可选)】时间富余就当堂跑一次 tools/std_probe.py,看同一段代码在 -std=c++98 和 c++17 下一个报错一个通过——"标准差异是可以实测的"。
|
||||
【过渡】"今天的 C++ 之旅到此,最后一小时:作业。明天早上 08:30,Qt。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 课堂作业 16:00–17:00(工程师答疑)',
|
||||
'title': '作业:修一个真实的坑 + 版本探针(选做)',
|
||||
'boxes': [
|
||||
('task', '任务卡 2 · 修一个真实的坑', [
|
||||
'目标:从 docs/ERRATA.md 挑 2 条与今天知识点相关的记录(虚析构/引用/重载类)',
|
||||
'验收:能用一句话向同组同学解释「wiki 原文错在哪、为什么」,不能只念代码',
|
||||
]),
|
||||
('task', '任务卡 3(选做)· 版本探针', [
|
||||
'目标:用 tools/std_probe.py 验证一个标准版本差异点',
|
||||
'验收:跑出「该标准报错、另一标准通过」的对比,对照 docs/VERSION_NOTES.md §2',
|
||||
]),
|
||||
],
|
||||
'minutes': 60, 'segment': '下午 · 作业',
|
||||
'notes': """【组织】作业开做前花 3 分钟带学员浏览一遍 ERRATA.md 的结构(每条:原文错误→修正→原因),示范挑一条"虚析构类"的怎么讲,给出"一句话解释"的示范标准。
|
||||
【巡场重点】拦住"念代码"式的讲解:追问"如果不改会发生什么?"逼出因果表述。这个表达训练到 Day10 答辩就能用上。
|
||||
【选做引导】完成快的学员推 std_probe:跑通一个对比后,引导他们把结论和 VERSION_NOTES.md 印证。
|
||||
【收尾】17:00 前每组出 1 人讲 1 条 ERRATA(每人 1 分钟内),当日验收随堂完成。"""
|
||||
},
|
||||
{
|
||||
'badge': '当日收尾',
|
||||
'title': '当日产出验证',
|
||||
'code': 'cmake --build build --target all # Part1,144 个目标全绿',
|
||||
'bullets': [
|
||||
'每人:构建全绿 + 2 条 ERRATA 坑点讲解(口头验收)',
|
||||
'PRETEST 结果归档,决定后续调速',
|
||||
'明天 Day2:Qt 概述 / 创建项目 / 第一个程序 / 信号槽 → 计算器',
|
||||
],
|
||||
'minutes': 5, 'segment': '收尾',
|
||||
'notes': """【操作】收尾 5 分钟:口头快速过验收清单;预告明天需要装隔离 Qt(tools/install_qt_host.sh),机器慢的可以现在挂着下载。
|
||||
【衔接】用一句话给今天收线:"今天所有内容都在讲对象的生与死。明天你们会看到,Qt 用一棵树管理成千上万个对象的生死。"留下钩子散场。"""
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,259 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Day2 · Qt 入门 / 信号槽 / 计算器案例 —— 页面对应 slides/day2.html。"""
|
||||
|
||||
SLIDES = [
|
||||
{
|
||||
'kind': 'title',
|
||||
'badge': 'C++/Qt 方向实训 · 第 2 / 4 个讲授日',
|
||||
'title': 'Day 2 · Qt 入门 / 信号槽 → 计算器',
|
||||
'subtitle': 'Qt 概述 / 创建项目 / 第一个程序 / 信号槽机制 / 案例:简易计算器',
|
||||
'bullets': [
|
||||
'上午 08:30–11:00 讲授:Qt 概述 · 创建 Qt 项目 · 第一个 Qt 小程序',
|
||||
'上午 11:00–12:00 课堂练习:跑通并改造最小程序(中文标题埋点)',
|
||||
'下午 13:30–16:00 讲授:信号槽三种写法 + 三张★坑卡 → 计算器组装',
|
||||
'下午 16:00–17:00 作业:完成简易计算器',
|
||||
],
|
||||
'source': '开始前一次性准备:tools/install_qt_host.sh && cmake -DBUILD_QT_PART=ON -DCMAKE_PREFIX_PATH=$PWD/.qt514/5.14.2/gcc_64',
|
||||
'minutes': 5, 'segment': '开场',
|
||||
'notes': """【开场】开课先集体执行"开始前准备"(装隔离 Qt 加重新配置构建)。装的等待时间正好用来讲第一页 Qt 概述,安排好并行,别干等。
|
||||
【时长骨架】上午:概述 20 → 创建项目 25 → 第一个程序 25 → 对象树 15 → 栈坑演示 15,共 100 分钟讲授加随堂跟敲,余量约 30 分钟给装环境收尾和答疑。下午:信号槽 35 → lambda 坑 15 → 重复 connect 12 → 编码 12 → 计算器组装讲解 25 + 当堂开工 30。
|
||||
【主线】今天的暗线是昨天的所有权:对象树就是 Qt 的所有权答案;三个★坑全是"生命周期"的变体。开场就把这条线挑明。"""
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': 'Qt 概述',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'Qt = 跨平台 C++ 应用框架:一份代码,Windows/Linux/macOS 原生编译',
|
||||
'模块化:本课程用 Core(对象/容器/IO)· Gui · Widgets(桌面控件)',
|
||||
'版本纪律:统一用隔离安装的 Qt 5.14.2(.qt514/),不碰系统 Qt',
|
||||
]),
|
||||
('info', '工程示范', [
|
||||
'顶层 CMakeLists 有版本断言,连错版本直接 FATAL_ERROR——「锁依赖版本」的真实做法',
|
||||
]),
|
||||
],
|
||||
'source': '出处:仓库根 CMakeLists.txt(版本断言一节)',
|
||||
'minutes': 20, 'segment': '上午 · 概述(可与装 Qt 并行)',
|
||||
'notes': """【讲法】"跨平台"用学员熟悉的软件举证(WPS、VirtualBox 界面都是 Qt);模块化用"按需取件"讲:Widgets 底下垫着 Gui,Gui 底下垫着 Core,链接哪些模块 CMake 里看得见。
|
||||
【版本纪律】这里讲的其实是工程课不是 Qt 课:为什么全班必须同一个 Qt 版本(示例行为一致、报错可复现),为什么用断言硬拦(出错要趁早,在配置期不在运行期)。打开顶层 CMakeLists 的断言给学员看一眼。
|
||||
【互动】问"谁的机器已经装完了?"用最快的一台跑 cmake 配置,看 Qt 5.14.2 被正确找到的输出长什么样,对照"找错版本会 FATAL_ERROR"。
|
||||
【过渡】"环境有了,怎么从零建一个 Qt 项目?下一页拆给你看。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': '创建 Qt 项目(CMake 方式)',
|
||||
'code': 'find_package(Qt5 5.14 REQUIRED COMPONENTS Core Gui Widgets)\nadd_executable(minimal_qt_app minimal_qt_app.cpp)\nset_target_properties(minimal_qt_app PROPERTIES AUTOMOC ON)\ntarget_link_libraries(minimal_qt_app PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'昨天的 find_package 今天落地:按 CMAKE_PREFIX_PATH 找到 Qt',
|
||||
'AUTOMOC 自动跑 moc(元对象编译器)——为 Q_OBJECT 类生成信号槽代码,下一页见',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/CMakeLists.txt · p03/ch02/CMakeLists.txt',
|
||||
'minutes': 25, 'segment': '上午 · 创建项目',
|
||||
'notes': """【讲法】四行 CMake 逐行讲:find_package 找什么(头文件+库+版本)、REQUIRED 意味着找不到就停、AUTOMOC 是给下页的 moc 留的钩子、target_link_libraries 对应模块化那页的"按需取件"。
|
||||
【moc 预热】这里先给 moc 一句话定义:"Qt 在编译前多跑的一个代码生成器",具体生成什么下一页配合 Q_OBJECT 讲。两页共用一个概念,分两次砸实。
|
||||
【随堂跟敲】让学员照着 p03/ch02/CMakeLists.txt 找到自己 build 目录里对应的 moc 产物文件(*_autogen),亲眼确认"moc 真的生成了代码"。这也是今晚钩子(读 moc_*.cpp)的铺垫。
|
||||
【过渡】"工程能编了,写第一个窗口。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': '第一个 Qt 小程序',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'QApplication 是唯一的事件循环拥有者,app.exec() 阻塞进入事件循环分发系统事件',
|
||||
'Q_OBJECT 宏触发 moc 生成信号槽/属性系统所需的元对象代码',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'一个进程只能有一个 QApplication;忘 exec() 窗口一闪而过',
|
||||
'用了信号槽却忘 Q_OBJECT:undefined reference——链接错误(昨天那页!),报错完全不提宏',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch02/minimal_qt_app.cpp',
|
||||
'minutes': 25, 'segment': '上午 · 第一个程序',
|
||||
'notes': """【讲法】当堂跑 minimal_qt_app,然后做两个"破坏实验":①注释掉 exec() 重新跑,窗口一闪而过,讲清 exec 前程序是"直播结束"状态;②讲事件循环用"前台服务员"类比:exec 就是服务员站上岗位开始接单(鼠标、键盘、重绘都是单子)。
|
||||
【Q_OBJECT 坑】这是今天最有价值的一个坑:口头承诺学员"你们两周内一定会撞上它"。撞上时的症状:undefined reference to vtable / 信号槽相关符号。回忆昨天"链接错误查定义",而这里的"定义"在 moc 生成的文件里,宏没写 moc 就不生成。修复:加宏 + 重新构建。
|
||||
【互动】问:"报错里一个字没提 Q_OBJECT,你怎么定位到它?"建立"链接错误 + Qt 类 ⇒ 先查宏"的条件反射。
|
||||
【过渡】"窗口有了,往里放控件。控件归谁管?昨天下午的问题来了。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': '控件创建与对象树',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'QObject 对象树 = Qt 的所有权答案(接昨天「谁负责 delete」)',
|
||||
'创建控件把 parent 传进去,parent 析构自动 delete 所有子对象——整棵树只管好根',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'忘传 parent:对象没人管,真实内存泄漏',
|
||||
'move() 手动定位的控件一旦套上布局会被接管覆盖',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch03/button_creation.cpp',
|
||||
'minutes': 15, 'segment': '上午 · 对象树',
|
||||
'notes': """【衔接】开头直接回指昨天:"昨天问'谁负责 delete',智能指针是一种答案,Qt 给了另一种:树。"两套答案解决同一个问题,这个对应关系点透。
|
||||
【讲法】画树:window → layout → buttons/labels;delete 根,全树倒序析构。强调工程收益:界面上百个控件,不写一个 delete。
|
||||
【规则】"交给树管的对象,不要再用智能指针/栈/手动 delete 管",一个对象只能有一个所有者。这句是下一页崩溃演示的谜底,先说规则再看崩溃。
|
||||
【过渡】"不信邪的写法长什么样?下一页现场崩一次。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00 · ★坑点复现',
|
||||
'title': '★ 栈对象与父子所有权打架',
|
||||
'code': 'QPushButton btn("我建在栈上"); // 先声明 -> 后析构\nQWidget window; // 后声明 -> 先析构\nbtn.setParent(&window);\n// 离开作用域:window 先析构 -> 父子树 delete &btn(栈地址!)-> 崩溃',
|
||||
'boxes': [
|
||||
('pitfall', None, [
|
||||
'声明顺序反过来时「恰好不崩」——不要赌析构顺序的运气,规则只有一条:给了 parent 就必须在堆上',
|
||||
]),
|
||||
],
|
||||
'source': '复现示例:examples/pitfall_stack_widget/(README 有对照讲解)· 演示:tools/run_qt.sh qt_pitfall_stack_widget --crash',
|
||||
'minutes': 15, 'segment': '上午 · 栈坑演示',
|
||||
'notes': """【演示流程】①先跑默认版(正确写法):窗口正常;②跑 --crash 版:终端打出 free(): invalid pointer 后 abort,让全班看真实崩溃现场;③回到代码逐行讲析构顺序:谁后声明谁先析构,window 析构时树机制 delete 了一个栈地址。
|
||||
【关键升华】把两行声明对调再跑(README 有说明):不崩了,但它仍然是错的。这里讲"UB 不等于必然崩溃":恰好不崩是最危险的状态,因为它能潜伏到交付。这个观念下午 lambda 坑还要用一次。
|
||||
【规则收束】黑板写下今天第一条军规:"给了 parent 的控件必须 new。"(Day3 讲对话框时这条军规会第三次出现。)
|
||||
【过渡】"上午最后一小时自己动手,顺便埋一个下午要用的雷(中文标题)。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 课堂练习 11:00–12:00(工程师指导、答疑)',
|
||||
'title': '任务卡 1 · 跑通并改造最小程序',
|
||||
'boxes': [
|
||||
('task', '任务卡 1', [
|
||||
'目标:编译运行 minimal_qt_app 与 button_creation;把窗口标题和按钮文字改成自己的学号+姓名(中文)',
|
||||
'验收:QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app 退出码 0',
|
||||
'验收:有 X 环境下自己改的中文标题正常显示',
|
||||
'参考:p03/ch02/minimal_qt_app.cpp · p03/ch03/button_creation.cpp',
|
||||
]),
|
||||
],
|
||||
'minutes': 60, 'segment': '上午 · 课堂练习',
|
||||
'notes': """【组织】前 20 分钟目标:全员把两个示例跑起来(上午刚装的环境在这里最终验收)。后 30 分钟做中文改造。
|
||||
【埋点说明(教师知悉,别提前剧透)】中文改造是给下午"QString 编码"坑卡埋的现场素材:部分学员会遇到乱码(源码编码或隐式转换),先只记录现象、告知"下午揭晓",不当场展开。带着问题听课效果最好。
|
||||
【常见卡点】CMAKE_PREFIX_PATH 没传导致 find_package 失败(回收上午创建项目页);改完代码没重新构建就跑旧程序(讲"改了要重编"的习惯)。
|
||||
【收尾】统计乱码人数,下午编码页开场直接用这个数字。"""
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': '信号槽:内置 / 自定义 / lambda',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'观察者模式的 Qt 实现:发送者只 emit,不知道谁在听——对象间彻底解耦',
|
||||
'新语法 &Class::signal 编译期检查;旧字符串语法 SIGNAL()/SLOT() 运行时检查',
|
||||
'连接方式 Direct/Queued/Auto(Day5 跨线程时回收闭环)',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'旧语法写错不报编译错误——运行时静默连接失败',
|
||||
'重载信号不加 QOverload 消歧义直接编译报错',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch04/builtin_signal_slot.cpp · custom_signal_slot.cpp · lambda_signal_slot.cpp',
|
||||
'minutes': 35, 'segment': '下午 · 信号槽',
|
||||
'notes': """【地位】信号槽是全部 Qt 课程的第一核心机制,本页 35 分钟是全天最大块,按"三个示例各 10 分钟 + 5 分钟总结"推进,每个示例都当堂跑。
|
||||
【讲法】①builtin:按钮 clicked 连内置槽,讲"信号=事件的语义化出口";②custom:自定义信号加 emit,讲"你的类也能发广播",顺便回收上午 Q_OBJECT/moc(信号的实现就在 moc 文件里);③lambda:匿名函数当槽,回收昨天现代 C++ 巡礼的 lambda——"昨天的新特性今天就用上了"。
|
||||
【新旧语法】演示一次旧语法故意拼错:编译通过、运行时只有一行警告、功能静默失效。对比新语法同样错误直接编译失败。结论:一律用新语法。
|
||||
【互动】"信号槽和直接调函数有什么区别?"解耦:发送者不 include 接收者,这是 Day5 案例架构分层的伏笔。
|
||||
【过渡】"lambda 好用,但它有一个能潜伏到上线的坑。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00 · ★坑点复现',
|
||||
'title': '★ lambda 槽的生命周期与第五参数',
|
||||
'code': "connect(sender, &Sender::sig, [obj]{ obj->f(); }); // X obj 销毁后仍触发\nconnect(sender, &Sender::sig, obj, [obj]{ obj->f(); }); // O 第五参数=context",
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'第五参数(context object)决定连接跟谁的生命周期挂钩:对象析构自动断开',
|
||||
'规则:lambda 捕获了某个 QObject,就把它作为第五参数传进去',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'不传 context:接收方销毁后 lambda 仍被调用——悬空指针,可能崩也可能「看起来正常」',
|
||||
]),
|
||||
],
|
||||
'source': '复现示例:examples/pitfall_lambda_capture/ · 演示:tools/run_qt.sh qt_pitfall_lambda_capture(--dangle 看真实悬空)',
|
||||
'minutes': 15, 'segment': '下午 · 信号槽坑卡',
|
||||
'notes': """【演示流程】跑默认版看确定性对比输出:delete 接收者后,无 context 的连接仍触发、带 context 的已自动断开,两条连接的差异一目了然。再跑 --dangle:真实悬空访问,本机实测打印出了"看似正常的旧值",正好接上午的话:"UB 恰好正常是最危险的状态",第二次强化。
|
||||
【规则】只给一条可执行军规(黑板第二条):"lambda 捕获了谁,第五参数就传谁。"不要展开讲连接对象的内部实现,中初级听众记规则比记原理牢。
|
||||
【回指】这就是昨天"悬空引用"的 Qt 版:对象死了,别人手里还留着它的地址。同一个病根第三次出现,点破这个重复是刻意设计。
|
||||
【过渡】"还有一个坑,症状更迷惑:功能是对的,就是每次都来两遍。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00 · ★坑点复现',
|
||||
'title': '★ 重复 connect:点一次按钮弹两次框',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'connect 每调一次新增一条连接,Qt 不去重——连两次,槽被调两次',
|
||||
'修复①connect 收拢到构造函数只做一次(首选);②Qt::UniqueConnection(仅成员函数指针,lambda 防不了)',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'connect 写在会被多次执行的路径上(每次打开界面都跑的「初始化」函数)——第一反应查按钮,根源在连接次数',
|
||||
]),
|
||||
],
|
||||
'source': '复现示例:examples/pitfall_double_connect/ · 演示:tools/run_qt.sh qt_pitfall_double_connect',
|
||||
'minutes': 12, 'segment': '下午 · 信号槽坑卡',
|
||||
'notes': """【演示】跑复现示例:输出清楚展示"重复 connect 后一次 emit 槽调 2 次 → UniqueConnection 后只调 1 次"。确定性输出,不用赌运气。
|
||||
【讲法】重点讲"事故现场的形态":没人会故意连两次,都是 connect 藏在 setup()/init() 这类会被反复调用的函数里。所以首选修复是结构性的,connect 全部收进构造函数(今晚计算器作业的验收标准里就有这条)。
|
||||
【UniqueConnection 的边界】强调它只认成员函数指针:lambda 没有身份,防不了。想用 UniqueConnection 就别用 lambda 槽,二选一说清楚。
|
||||
【过渡】"最后一个坑不崩也不重复,它只是让你的界面变成'????'。上午改中文乱码的同学,谜底来了。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00 · ★坑卡',
|
||||
'title': '★ QString 与中文编码',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'QString 内部是 UTF-16,和 std::string(字节串)是两种东西',
|
||||
'互转必须指明编码:toUtf8() / fromUtf8();中文字面量用 QStringLiteral + 源码存 UTF-8',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'源码不是 UTF-8、或用 QString(char*) 隐式转换中文字面量 -> 界面乱码',
|
||||
'Day4 读写文件时编码问题还会再来一次(同一根源)',
|
||||
]),
|
||||
],
|
||||
'minutes': 12, 'segment': '下午 · 编码坑卡',
|
||||
'notes': """【开场】直接报上午统计的乱码人数,请一位中招学员描述现象,用真实案例开讲比抽象讲编码有效得多。
|
||||
【讲法】画"两个世界"图:字节世界(std::string/文件/网络)和文字世界(QString/界面),中间隔一道墙,过墙必须声明编码。乱码的本质:过墙时没声明,系统猜错了。
|
||||
【可执行规则】三条:源码文件一律存 UTF-8;中文字面量写 QStringLiteral("…");和 std::string 互转显式 fromUtf8/toUtf8。让中招学员当堂改好自己上午的代码作为验证。
|
||||
【预告】Day4 文件读写会遇到同一堵墙的另一个门(QTextStream 默认编码随平台变),到时回收。
|
||||
【过渡】"四个坑讲完,弹药齐了。开始组装今天的正菜:计算器。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00 · 案例',
|
||||
'title': '案例 · 简易计算器(布局脚手架:先用后学)',
|
||||
'code': "auto* grid = new QGridLayout(window); // 布局原理明天下午讲透\ngrid->addWidget(btn7, 0, 0); grid->addWidget(btn8, 0, 1); // 今天先会用:\ngrid->addWidget(btn9, 0, 2); grid->addWidget(btnDiv, 0, 3); // 行、列坐标摆键盘",
|
||||
'boxes': [
|
||||
('concept', '组装思路', [
|
||||
'按钮(上午)+ 点击信号槽(刚学)+ QGridLayout 脚手架排键盘 + QLabel/QLineEdit 显示结果',
|
||||
'项目 = 已学知识点的组装——Day5 综合案例和 Day6–10 项目的预演',
|
||||
]),
|
||||
],
|
||||
'minutes': 25, 'segment': '下午 · 计算器组装讲解',
|
||||
'notes': """【讲法】自顶向下带一遍设计再放手:①界面拆解(显示区加 4×4 键盘);②交互拆解(数字键追加、运算键记住操作数和运算符、等号算);③把每块映射到今天学的知识点。白板画完设计图再看脚手架代码。
|
||||
【脚手架声明】明确"先用后学"的边界:今天只需要会 addWidget(w, row, col) 这一个动作。为什么它能自适应、还有哪些布局,明天下午专门讲,防止有人今天就钻布局细节出不来。
|
||||
【实现提示】给一个最省事的状态机设计(当前值/暂存值/待执行运算符三个成员变量),但不给完整代码,作业要自己写。演示教师版成品跑一次,明确"长这样就算达标"。
|
||||
【剩余时间】讲解 25 分钟后,剩下约 30 分钟当堂开工,作业时段继续。"""
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 课堂作业 16:00–17:00(工程师答疑)',
|
||||
'title': '任务卡 2 · 简易计算器',
|
||||
'boxes': [
|
||||
('task', '任务卡 2', [
|
||||
'目标:组合按钮 + 信号槽 + QGridLayout 脚手架,做出能加减乘除的最小计算器',
|
||||
'验收:两个数 + 一种运算符计算正确并显示;界面不重叠、按钮可点击',
|
||||
'验收:connect 全部用新语法且写在构造函数里(今天的坑卡现学现用)',
|
||||
'参考:p03/ch03/button_creation.cpp · p03/ch04/custom_signal_slot.cpp · p03/ch08/custom_widget.cpp(布局参考)',
|
||||
]),
|
||||
],
|
||||
'minutes': 60, 'segment': '下午 · 作业',
|
||||
'notes': """【组织】独立完成,允许组内讨论思路但代码各写各的。教师端备好参考实现(不发放)。
|
||||
【巡场检查点】对照三张坑卡验收:connect 是否都在构造函数(防重复连接)、有没有栈上建控件、中文按钮文字是否正常。坑卡内容当天就进验收,记忆效果最好。
|
||||
【分层】快的学员加"连续运算"或"清除键 C";慢的学员先保证"两个数一个运算符"最小闭环,键盘可以只做 0–9 和 + =。
|
||||
【预告】明天计算器不重写,直接在今天的代码上套 QMainWindow、加菜单、上 QSS。今天的代码质量决定明天的起点,散场前提醒提交保存。"""
|
||||
},
|
||||
{
|
||||
'badge': '当日收尾',
|
||||
'title': '当日产出验证',
|
||||
'bullets': [
|
||||
'现场演示:计算器完成至少一次正确运算;中文界面无乱码',
|
||||
'钩子:打开 build/ 里真实的 moc_*.cpp,看 moc 生成了什么代码',
|
||||
'明天 Day3:QMainWindow / QDialog / 布局讲透 / 常用控件 / QSS——计算器升级成「正经软件」',
|
||||
],
|
||||
'minutes': 5, 'segment': '收尾',
|
||||
'notes': """【验收】抽 2–3 人现场演示计算器运算;全班自查中文无乱码。
|
||||
【钩子讲法】当堂打开一个 moc_*.cpp 滚一屏,不逐行讲,只说一句:"信号槽没有魔法,全在这份生成代码里,感兴趣的回去读。"能看懂的学员会对元对象系统祛魅。
|
||||
【收线】"今天你们写出了第一个有交互的 Qt 程序;明天给它一个正经软件的骨架。"散场。"""
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,190 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Day3 · QMainWindow / QDialog / 布局与控件 / QSS —— 页面对应 slides/day3.html。"""
|
||||
|
||||
SLIDES = [
|
||||
{
|
||||
'kind': 'title',
|
||||
'badge': 'C++/Qt 方向实训 · 第 3 / 4 个讲授日',
|
||||
'title': 'Day 3 · QMainWindow / QDialog / 布局与控件',
|
||||
'subtitle': '主窗口骨架 / 对话框交互 / 布局讲透 / 常用控件 / QSS 美化',
|
||||
'bullets': [
|
||||
'上午 08:30–11:00 讲授:QMainWindow · 对话框 QDialog · 标准对话框',
|
||||
'上午 11:00–12:00 课堂练习:计算器套上 QMainWindow(菜单 + 停靠窗)',
|
||||
'下午 13:30–16:00 讲授:布局管理器(回收脚手架)· 常用控件与 .qrc · QSS',
|
||||
'下午 16:00–17:00 作业:QSS 美化计算器 + 缩放自适应',
|
||||
],
|
||||
'minutes': 3, 'segment': '开场',
|
||||
'notes': """【主线】今天全天围绕一件事:把昨天的计算器升级成"正经软件"。上午给骨架(主窗口加对话框),下午管排版和外观。开场就展示教师版的"升级后计算器"成品,让学员看到今天结束时自己的程序长什么样,目标可视化。
|
||||
【时长骨架】上午:QMainWindow 30 → QDialog 模态非模态 30 → 标准对话框 15,讲授约 75 分钟,剩余给随堂跟敲和答疑。下午:布局 30 → 控件与 qrc 25 → QSS 25,随堂练穿插。
|
||||
【衔接】昨天的"堆上建控件"军规今天在对话框一节会第三次出现,留意把这条线讲成呼应而不是重复。"""
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': 'QMainWindow:标准桌面软件的骨架',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'QMainWindow 不是「大号 QWidget」:菜单栏/工具栏/状态栏/停靠窗各就其位的固定区域协议',
|
||||
'中心部件必须 setCentralWidget() 显式安放',
|
||||
'菜单项和工具栏按钮的统一抽象是 QAction——一个动作,多处复用',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'不设中心部件直接摆控件——布局行为不可预期',
|
||||
'QAction 显示出来 ≠ 生效:忘 connect 就是「菜单点了没反应」',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch05/central_widget.cpp · dock_widget.cpp',
|
||||
'minutes': 30, 'segment': '上午 · QMainWindow',
|
||||
'notes': """【引入】投一张学员都认识的软件截图(VS Code / WPS),指认五个区域:菜单/工具/状态/中心/停靠。"QMainWindow 就是这套行业标准布局的现成实现。"
|
||||
【讲法】跑 central_widget.cpp 与 dock_widget.cpp,边跑边对照代码:setCentralWidget、menuBar()->addMenu、addToolBar、statusBar()->showMessage 四个入口各演示一次。
|
||||
【QAction 重点】用"一个动作三个入口"讲:同一个 QAction 同时挂菜单和工具栏,禁用它两处一起变灰,这就是抽象的价值。坑卡里"菜单点了没反应"发生率极高,让学员现在就形成"建 action 后第一件事 connect"的顺序习惯。
|
||||
【互动】问:"中心部件只能有一个,那复杂界面怎么办?"答案是中心部件本身可以是任意组合控件(下午布局课解决),先埋问题。
|
||||
【过渡】"主窗口是常驻界面;临时冒出来问一句的界面叫对话框。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': 'QDialog:模态与非模态',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'模态 exec():阻塞等结果,返回值就是用户的选择(Accepted/Rejected)',
|
||||
'非模态 show():立即返回,窗口独立存活——对象必须活过当前函数作用域',
|
||||
'非模态三种写法:堆 + parent / WA_DeleteOnClose 自清理 /(反例)栈',
|
||||
]),
|
||||
('pitfall', '常见的坑(所有权军规第三次出现!)', [
|
||||
'非模态对话框建在栈上:show() 完函数一返回就析构——窗口一闪而过',
|
||||
'p03/ch06 的三个 modeless 版本就是这个坑的对照教材',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch06/modal_dialog.cpp · modeless_dialog_stack.cpp · _heap.cpp · _deleteonclose.cpp',
|
||||
'minutes': 30, 'segment': '上午 · QDialog',
|
||||
'notes': """【讲法】模态/非模态先用生活类比定性:模态=收银台付款(不办完不能干别的),非模态=旁边开着的计算器(随时切换)。然后讲代码差异的本质:exec() 内部自己转事件循环所以能阻塞,show() 只是显示。
|
||||
【三连跑对照】依次跑 ch06 三个 modeless 文件:栈版一闪而过(学员先猜原因,Day2 军规该有人能答上)、堆版正常但要考虑谁删、DeleteOnClose 版关窗自清理。同一功能三种生命周期策略并排,是全仓库最好的所有权教材。
|
||||
【收束】黑板军规更新:"给了 parent 的控件必须 new"加"非模态对话框必须活过函数"。本质是同一条:对象的生命周期必须覆盖它的使用周期。
|
||||
【过渡】"自己写对话框之前,先看看 Qt 送了哪些现成的。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': '标准对话框速览',
|
||||
'code': 'auto ret = QMessageBox::question(this, "确认", "真的要删除吗?");\nif (ret == QMessageBox::Yes) { /* ... */ }\n\nQString path = QFileDialog::getOpenFileName(this, "选择文件", "", "*.json");\nif (!path.isEmpty()) { /* ... */ } // 用户可能点取消:必须处理空返回',
|
||||
'boxes': [
|
||||
('pitfall', None, [
|
||||
'不处理「用户点了取消」(空路径/Rejected)就直接往下用——健壮性问题高发点',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch06/message_box_question.cpp · message_box_custom.cpp · file_dialog.cpp',
|
||||
'minutes': 15, 'segment': '上午 · 标准对话框',
|
||||
'notes': """【讲法】两个静态函数各演示一次即可,重点全放在坑卡上:让学员齐声回答"getOpenFileName 之后第一行代码写什么?"判空。用户永远可能点取消,这是健壮性思维的第一课(Day4 的 open() 判返回值、ParseError 检查是同一条思维线)。
|
||||
【提示】QMessageBox 的 question/information/warning/critical 四兄弟提一嘴图标差异即可,不逐个演示。
|
||||
【过渡】"骨架和对话框都有了。上午最后一小时,把你的计算器装进这副骨架。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 课堂练习 11:00–12:00(工程师指导、答疑)',
|
||||
'title': '任务卡 1 · 给计算器套上 QMainWindow',
|
||||
'boxes': [
|
||||
('task', '任务卡 1', [
|
||||
'目标:昨天的计算器改造为 QMainWindow 应用;加菜单「帮助→关于」弹 QMessageBox;加「历史」停靠窗(可先放空 QLabel)',
|
||||
'验收:计算器功能不回退;菜单动作有响应;停靠窗可拖动/收起',
|
||||
'参考:p03/ch05/central_widget.cpp · dock_widget.cpp · p03/ch06/message_box_question.cpp',
|
||||
]),
|
||||
],
|
||||
'minutes': 60, 'segment': '上午 · 课堂练习',
|
||||
'notes': """【关键改造思路(先讲 3 分钟再放手)】昨天的计算器是一个 QWidget,今天不重写,把它整个 setCentralWidget 进 QMainWindow。这个"旧组件装进新骨架"的动作本身就是组件化思维的第一次实践,点破它。
|
||||
【巡场检查点】①菜单 QAction 忘 connect(上午坑卡现场版,让学员自己对照坑卡定位);②功能回退(改造弄坏了昨天的计算逻辑,借机讲"改结构不改逻辑"的重构纪律)。
|
||||
【分层】快的学员让"历史"停靠窗真的记录每次运算(QLabel 追加文本即可),这直接为 Day5 案例的表格记录做铺垫。
|
||||
【收尾】抽 1–2 人演示:点菜单弹关于、拖动停靠窗。"""
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': '布局管理器(回收昨天的脚手架)',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'布局接管子控件位置/大小计算,随窗口缩放自动重排',
|
||||
'QHBox / QVBox / QGrid 三兄弟 + 嵌套组合 + 伸缩因子 addStretch()',
|
||||
'布局对象同样遵循 QObject 父子所有权',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'同一控件不能属于两个布局',
|
||||
'布局设置后又用 setGeometry()/move() 手动改位置——被下次刷新覆盖,看起来「设置没生效」',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch08/custom_widget.cpp',
|
||||
'minutes': 30, 'segment': '下午 · 布局',
|
||||
'notes': """【兑现承诺】开场明确"昨天欠的布局课现在还":把计算器脚手架里的 QGridLayout 拿出来讲原理。为什么 addWidget(w, row, col) 之后拉伸窗口按钮会跟着变?因为布局在每次 resize 时重算所有子控件的几何。
|
||||
【讲法】演示三兄弟各一个最小例子,然后重点讲嵌套:垂直布局套水平布局,拼出"上显示屏下键盘"的计算器结构。学员昨天已经无意识用过,今天补上心智模型。addStretch 用"弹簧"类比。
|
||||
【坑卡】当堂演示"布局后 move() 失效":move 完拉伸一下窗口,控件跳回布局位置,"看起来没生效"的真相是被布局覆盖。规则:进了布局就别手动定位,二选一。
|
||||
【随堂练 10 分钟】把自己计算器的布局改成"显示屏占 1 份、键盘占 3 份"的比例(伸缩因子)。
|
||||
【过渡】"位置管好了,看控件本身还有什么花样。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': '常用控件 + 资源系统 .qrc',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'QLabel 一个控件多种形态:纯文本 / 富文本 HTML / 图片 QPixmap / 动图 QMovie',
|
||||
'输入靠 QLineEdit;按钮族 QPushButton / QRadioButton / QCheckBox',
|
||||
':/... 路径是 Qt 资源系统:资源编译期打包进可执行文件,发布不用带散图',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'.qrc 路径与代码引用不一致:运行时静默失败(图不显示),不报编译错误',
|
||||
'忘接 CMake 的 AUTORCC / qt5_add_resources——资源系统不生效',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch08/label_text_html.cpp · label_pixmap.cpp · label_movie.cpp',
|
||||
'minutes': 25, 'segment': '下午 · 控件与资源',
|
||||
'notes': """【讲法】三个 label 示例连跑(文本→HTML→图片/动图),节奏快一点,每个 5 分钟:控件 API 本身不难,难的是"知道有什么可用",本页目标是建目录索引不是背 API。
|
||||
【qrc 重点】打开 label_pixmap.qrc 和对应 cpp 对照讲:qrc 里登记路径 → 代码里 :/ 引用 → CMake AUTORCC 编译打包。坑卡演示:把代码里的 :/ 路径改错一个字母再跑,不报错、图消失,"静默失败"要亲眼见过才会警惕(Day4 的 open()/ParseError 静默失败是同一主题)。
|
||||
【互动】问:"图片放资源系统 vs 放程序旁边的目录,各适合什么?"只读小资源进 qrc;用户数据/大文件放文件系统(Day4 的主题,顺势预告)。
|
||||
【过渡】"功能齐了,长得还不行。最后一节,化妆。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': 'QSS 界面美化',
|
||||
'code': 'QPushButton#okButton { background-color: #2f855a; border-radius: 6px; }\nQPushButton#okButton:hover { background-color: #276749; }',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'语法脱胎于 CSS:类型选择器 / ID 选择器(#objectName)/ 伪状态(:hover :pressed)+ 层叠',
|
||||
'作用对象是 QWidget 树而非 DOM;「长什么样」与「做什么」分离,改样式不碰逻辑',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'ID 选择器必须先 setObjectName()',
|
||||
'setStyleSheet() 级联影响所有子控件——范围失控最易踩,样式尽量写窄(带选择器)',
|
||||
]),
|
||||
],
|
||||
'source': '出处:examples/qss_styling/qt_qss_styling.cpp(README 有练习方向)',
|
||||
'minutes': 25, 'segment': '下午 · QSS',
|
||||
'notes': """【讲法】跑 qss_styling 示例,然后当堂改样式立即重跑,"改外观不碰逻辑"的分离感是本节要传达的核心体验。学过 CSS 的学员点一句"就是 CSS 的方言",没学过的按"选择器=谁、属性=怎么画"从零讲。
|
||||
【坑卡演示】级联失控:在顶层 widget 上裸写 background-color(不带选择器),跑起来所有子控件全变色,然后加上类型选择器收窄。让学员记"样式写窄"三个字。
|
||||
【伪状态】:hover/:pressed 是"零代码交互反馈",性价比极高,今晚作业的验收点之一。
|
||||
【随堂练 15 分钟】给上午的主窗口版计算器套第一版 QSS(哪怕只改按钮底色),作业时段继续精修。
|
||||
【过渡】"今晚作业就是把它做完做好,验收标准在下一页。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 课堂作业 16:00–17:00(工程师答疑)',
|
||||
'title': '任务卡 2 · 用 QSS 美化计算器',
|
||||
'boxes': [
|
||||
('task', '任务卡 2', [
|
||||
'目标:数字键/运算键/等号键用 QSS 区分颜色;整体布局随窗口缩放自适应',
|
||||
'验收:至少 2 类按钮不同 background-color;至少 1 个按钮有 :hover 或 :pressed',
|
||||
'验收:拉伸窗口按钮跟着重排',
|
||||
'参考:examples/qss_styling/ · p03/ch08/custom_widget.cpp',
|
||||
]),
|
||||
],
|
||||
'minutes': 60, 'segment': '下午 · 作业',
|
||||
'notes': """【实现提示(开工前讲 3 分钟)】三类按键区分的正确姿势:按类别 setObjectName(或用动态属性),QSS 里 ID/属性选择器分别上色。不要给每个按钮单独 setStyleSheet(层级混乱且难维护)。
|
||||
【巡场检查点】①样式写太宽把显示屏也染色了(下午坑卡现场版);②hover 无效,大概率 objectName 没设或拼错,静默失败,让学员自己按坑卡排查。
|
||||
【分层】快的学员挑战"深色主题"整套配色,或给等号键加 :pressed 按下变暗的两态反馈。
|
||||
【预告】明天计算器线暂停一天(事件/绘图/文件),Day5 综合案例会用到今天所有的骨架加布局加美化技能。"""
|
||||
},
|
||||
{
|
||||
'badge': '当日收尾',
|
||||
'title': '当日产出验证',
|
||||
'bullets': [
|
||||
'现场演示:主窗口版计算器(菜单/停靠窗)+ QSS 配色 + 缩放自适应,功能不回退',
|
||||
'钩子:QSS 与前端 CSS 同源;Qt Designer / QML 是另一条 UI 路线(知道存在即可)',
|
||||
'明天 Day4:事件机制 / 绘图 / 表格 + 文件系统——数据要开始落盘了',
|
||||
],
|
||||
'minutes': 5, 'segment': '收尾',
|
||||
'notes': """【验收】抽 2–3 组演示升级后的计算器,其他人对照验收标准自查打钩。
|
||||
【钩子讲法】一句话安放 Designer/QML:"拖拽和声明式是另外两条 UI 路线,本课程走纯代码是为了让你们看清每一层。会走路了再学开车不迟。"
|
||||
【收线】"三天了,你们的程序还没碰过磁盘。明天开始,让数据活过程序的重启。"散场。"""
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,230 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Day4 · 事件机制 / 绘图与表格 / 文件系统 —— 页面对应 slides/day4.html。"""
|
||||
|
||||
SLIDES = [
|
||||
{
|
||||
'kind': 'title',
|
||||
'badge': 'C++/Qt 方向实训 · 第 4 / 4 个讲授日',
|
||||
'title': 'Day 4 · 事件机制 / 绘图与表格 / 文件系统',
|
||||
'subtitle': 'Qt 消息机制和事件 / 绘图事件及表格事件 / 文件系统 + JSON',
|
||||
'bullets': [
|
||||
'上午 08:30–11:00 讲授:事件循环 · 事件拦截 · QPainter 绘图 · QTableWidget 表格',
|
||||
'上午 11:00–12:00 课堂练习:事件过滤器 + 自绘图形',
|
||||
'下午 13:30–16:00 讲授:QFile/流/JSON + ★工作目录 + ★文件编码',
|
||||
'下午 16:00–17:00 作业:本地数据存储小工具(Day5 综合案例前身)',
|
||||
],
|
||||
'minutes': 3, 'segment': '开场',
|
||||
'notes': """【主线】今天上下午各一条线:上午"框架怎么把外部世界的变化送进你的代码"(事件加绘图加表格),下午"你的数据怎么活过程序的重启"(文件加 JSON)。今晚的作业「本地数据存储小工具」就是明天综合案例的直接前身,做完今天,明天的案例就是把今天的成果组装放大。
|
||||
【时长骨架】上午:事件循环 15 → 事件拦截 25 → 绘图 25 → 表格 20,讲授约 85 分钟。下午:QIODevice 20 → JSON 25 → 工作目录坑 15 → 文件编码坑 15,讲授约 75 分钟。两段都留足随堂练时间。
|
||||
【衔接提醒】上午事件循环页要给下午的"主线程阻塞=冻结"埋点,下午埋的雷明天 QThread 来拆,一条贯穿三天的暗线。"""
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': '事件循环与派发:事件 vs 信号',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'Day2 的 app.exec() 拆开看:事件循环不停从队列取 QEvent 派发给目标控件',
|
||||
'事件 = 框架推给控件的通知(鼠标/键盘/重绘);信号槽 = 对象间主动通知',
|
||||
'两套机制并存不冲突',
|
||||
]),
|
||||
('pitfall', '明天的伏笔', [
|
||||
'主线程做耗时操作 = 事件循环停摆 = 整个界面冻结',
|
||||
'学员项目最高频的「卡死」来源,正确解法(多线程)明天综合案例讲',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch02/minimal_qt_app.cpp(exec)',
|
||||
'minutes': 15, 'segment': '上午 · 事件循环',
|
||||
'notes': """【衔接】回指 Day2"事件循环=前台服务员"的类比,今天把它拆开:服务员从队列里取单子(QEvent)派给对应窗口。然后区分两套机制:事件是"系统/框架主动通知控件",信号槽是"对象之间互相通知",同一个按钮点击会同时产生鼠标事件和 clicked 信号,两者不冲突。
|
||||
【坑卡只埋不拆】主线程阻塞坑今天只演示现象不解决:在按钮槽里塞一个 sleep(5) 的循环跑给学员看,窗口冻住、拖不动、系统提示未响应。明确说"这是明天综合案例的核心提升点,今天的任务是认清病症"。埋点到位,明天 QThread 出场时学员会有"终于等到"的投入感。
|
||||
【过渡】"事件派给控件之后,控件怎么处理?两条路。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': '事件拦截:重写 xxxEvent vs 事件过滤器',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'两条拦截路径:重写 mousePressEvent() 等虚函数(只影响自己)',
|
||||
'installEventFilter()(一个对象统一拦截多个控件的事件)',
|
||||
'事件有 accept()/ignore() 状态,决定是否继续向父控件传播',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'重写事件函数忘记调用基类实现(QWidget::mousePressEvent(e))——控件默认行为悄悄丢失',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch09/event_override_customwidget.cpp · event_filter_mainwindow.cpp · mouse_event.cpp',
|
||||
'minutes': 25, 'segment': '上午 · 事件拦截',
|
||||
'notes': """【讲法】两条路径用"自己处理 vs 雇保安"类比:重写 xxxEvent 是自己加装门铃(只响自己家门),installEventFilter 是给整栋楼雇一个保安(一个对象拦截多个控件)。各跑一个示例对照。
|
||||
【坑卡演示】重写 mousePressEvent 不调基类版本:跑一下,发现双击不再选中文字、右键菜单没了,"默认行为悄悄丢失"。修复就是加一行 QWidget::mousePressEvent(e)。这个坑在学员项目里命中率非常高,必须当堂见过。
|
||||
【accept/ignore】只讲概念不深挖:事件先派给最内层控件,ignore() 后会向父控件冒泡,为 Day5 综合案例"关闭事件弹确认框"埋点(closeEvent)。
|
||||
【过渡】"有一种特殊事件叫重绘事件,它的处理方式和其他事件不一样。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': '绘图事件:QPainter',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'Qt 绘制是框架驱动而非「想画就画」:所有绘制只能发生在 paintEvent() 里',
|
||||
'由 Qt 按需调用;数据变了想重画,调 update()(异步排队一次重绘请求)',
|
||||
'绘图设备三件套 QImage / QPixmap / QPicture 作参考',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'在 paintEvent 之外直接构造 QPainter 画窗口——未定义行为',
|
||||
'数据变了忘调 update()——界面不会自动重画',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch10/qpainter_basic.cpp(设备类参考:paint_device_qimage.cpp 等)',
|
||||
'minutes': 25, 'segment': '上午 · 绘图',
|
||||
'notes': """【核心理念】这页最重要的一句话是"框架驱动而非想画就画",和学员过去"想画就 printf"的命令式心智彻底不同。用"只能在画室里画画,画室开门由管理员决定"类比 paintEvent 的被动性。
|
||||
【讲法】跑 qpainter_basic.cpp(画几个基本图形),然后演示 update() 的必要性:改一个数据成员不调 update,界面没反应;加上 update(),重画。这是上午随堂练的核心动作。
|
||||
【坑卡】paintEvent 外构造 QPainter 是 UB,简单说一句"别在按钮槽里直接画窗口"即可,不展开。
|
||||
【应用预告】绘图在项目里的典型用场是自绘控件(仪表盘、波形图),告诉学员"想做漂亮图表的,这是入口",给有兴趣的人留方向。
|
||||
【过渡】"刚才画的都是固定图形;要展示表格状数据,用另一种控件。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 讲授 08:30–11:00',
|
||||
'title': '表格:QTableWidget 与单元格事件',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'item 类控件:数据直接塞进控件(QTableWidgetItem,new 出来交给表格接管所有权)',
|
||||
'"表格事件"以信号暴露:cellClicked / cellDoubleClicked / itemChanged',
|
||||
]),
|
||||
('info', '钩子 · 背后是完整的 Model/View 架构', [
|
||||
'数据(model)与展示(view)彻底分离、同一模型可喂多个视图',
|
||||
'改模型必须发 dataChanged 等信号视图才刷新——数据规模大/多视图联动时的正解',
|
||||
'自学入口:examples/model_view/',
|
||||
]),
|
||||
],
|
||||
'source': '出处:examples/table_widget/qt_table_widget_basics.cpp(README)',
|
||||
'minutes': 20, 'segment': '上午 · 表格',
|
||||
'notes': """【讲法】跑 table_widget 示例:填充 3×3 加点击单元格看状态栏变化加双击编辑触发 itemChanged。重点讲 QTableWidgetItem 的所有权,又是 new 出来交给控件(QObject 父子所有权的第 N 次出现,点一句即可不再展开)。
|
||||
【两条路线的边界】QTableWidget(item 类)vs Model/View:用"自带数据的成品表格"vs"数据和展示分离的乐高"类比。明确今天用前者,上手快、中小项目够用;后者作为钩子,数据量大或多视图联动时再学。
|
||||
【钩子讲法】Model/View 只用一页概念页带过,不演示完整代码:打开 model_view 示例让学员看一眼"同一个模型喂 QListView 和 QTableView",证明分离的价值即可。深入留给自学或 Day5 案例。
|
||||
【互动】问"点击表头怎么响应?"指向 cellClicked 之外的 header sectionClicked 信号,培养"需求→查信号"的检索意识。
|
||||
【过渡】"上午最后一小时,自己拦事件、自己画图形。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 · 课堂练习 11:00–12:00(工程师指导、答疑)',
|
||||
'title': '任务卡 1 · 事件过滤器 + 自绘图形',
|
||||
'boxes': [
|
||||
('task', '任务卡 1', [
|
||||
'目标①:给一个控件装事件过滤器,拦截鼠标按下并打印坐标',
|
||||
'目标②:在 paintEvent 里画一个组合图形(棋盘格或仪表盘刻度)',
|
||||
'验收:点击控件终端能看到坐标输出;图形正确绘制(可截图验证)',
|
||||
'参考:p03/ch09/event_filter_mainwindow.cpp · p03/ch10/qpainter_basic.cpp',
|
||||
]),
|
||||
],
|
||||
'minutes': 60, 'segment': '上午 · 课堂练习',
|
||||
'notes': """【组织】两个子任务相互独立,可分组并行:A 组做事件过滤器(练"雇保安"路径),B 组做自绘(练 paintEvent + update 闭环),各 30 分钟后交叉展示。
|
||||
【巡场检查点】①事件过滤器里忘调基类 eventFilter 返回 false 导致默认行为异常(上午坑卡现场版);②自绘改了数据忘 update(上午坑卡现场版);③事件过滤器忘 installEventFilter 安装,编译过但拦截不生效。
|
||||
【分层】快的学员把两题合一:自绘一个仪表盘,鼠标点击改变指针角度并立即重绘,这就是 Day5 综合案例级别的交互雏形。
|
||||
【收尾】每组演示一次成果即可,不要求两题都完成。"""
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': 'QIODevice 统一读写抽象',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'QFile / QTextStream / QDataStream 都建立在 QIODevice 统一接口上',
|
||||
'之后遇到网络/串口也是同一套接口',
|
||||
'QTextStream 面向文本行;QDataStream 面向结构化二进制',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'open() 返回值不检查——文件不存在/没权限时静默失败,后续读写全是空操作',
|
||||
'文本/二进制模式混淆会破坏内容',
|
||||
]),
|
||||
],
|
||||
'source': '出处:p03/ch11/qfile_basic.cpp · qtextstream_io.cpp · qdatastream_io.cpp',
|
||||
'minutes': 20, 'segment': '下午 · 文件 IO',
|
||||
'notes': """【引入】从"程序关掉数据就没了"切入:要数据活过重启,必须落到磁盘,这就是文件 IO 的存在理由。
|
||||
【讲法】QIODevice 抽象用"统一插头"讲:今天接文件、明天接网络、后天接串口,API 都是 open/read/write/close,投资一次受益终身。三个示例各演示读写一对:qfile_basic(裸字节)、qtextstream(按行文本)、qdatastream(结构化二进制,写进去再读出来类型不丢)。
|
||||
【坑卡演示】open() 不判返回值:把文件名改成一个不存在的路径再跑,程序不报错,写进去的数据全丢(写到一个失败的 QFile 上)。修复就是 if (!file.open(...)) return; 这一行。这是健壮性思维第二课(接上午 QMessageBox 判空)。
|
||||
【过渡】"文本/二进制流都会用,但项目里数据最常落地成什么格式?JSON。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00',
|
||||
'title': 'JSON 解析与写入',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'序列化/反序列化是一对互逆操作,正确性标准 = round-trip:写出去再读回来数据一致',
|
||||
'QJsonDocument / Object / Array / Value 四件套;QJsonValue 是能装多种类型的变体',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'不检查 QJsonParseError:格式坏的输入静默变成空文档',
|
||||
'类型不匹配时 toInt()/toString() 不报错只返回默认值——数据悄悄丢,很难查',
|
||||
]),
|
||||
],
|
||||
'source': '出处:examples/json_io/qt_json_parse_write.cpp(README)',
|
||||
'minutes': 25, 'segment': '下午 · JSON',
|
||||
'notes': """【讲法】round-trip 是本页核心:用"打包寄出再拆包"类比序列化/反序列化,正确性判据不是"能生成文件"而是"写出去读回来字段一个不丢"。这个判据今晚作业的验收标准就是它。
|
||||
【四件套】画树状结构讲清层级:Document 包 Object/Array,Object 是键值对,Array 是列表,Value 是叶子(可能是 string/number/bool/object/array)。跑 json_io 示例对照代码看每一层。
|
||||
【坑卡演示】两个静默失败各演示一次:①喂一段格式错的 JSON 不检查 ParseError,得到空文档,程序继续跑还以为没数据;②toInt() 取一个字符串字段,不报错返回 0,"数据悄悄丢"。这两个是 JSON 教学最有价值的两次当堂踩坑。
|
||||
【过渡】"读写都会了,但很多同学一跑就发现:'文件找不到'。下一页是几乎每届必问的坑。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00 · ★坑卡',
|
||||
'title': '★ 相对路径与工作目录',
|
||||
'code': 'QFile bad("data.json"); // X 相对当前工作目录——它在哪取决于你怎么启动\nQFile good(QCoreApplication::applicationDirPath() + "/data.json"); // O 锚定可执行文件位置',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'相对路径基于进程当前工作目录——不是源码目录,也不一定是可执行文件目录',
|
||||
'IDE、终端、脚本三种启动方式的工作目录可能各不相同',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'"IDE 里能读到文件,命令行跑就说不存在"——几乎每届学员必问,根源就是这页',
|
||||
]),
|
||||
],
|
||||
'minutes': 15, 'segment': '下午 · 路径坑',
|
||||
'notes': """【痛点定位】这一页是 Day4 学员呼声最高的一页:作业一开做就会有人喊"我代码和你一样为什么读不到文件"。把它单独成页就是为了让全班同时建立"工作目录"意识。
|
||||
【讲法】当堂用三种方式跑同一个程序(IDE 跑、终端在源码目录跑、终端在 build 目录跑),分别 pwd 看工作目录,同一个相对路径读到的是三个不同位置的文件。根因讲清:相对路径不锚定可执行文件,锚定的是"启动时所在的目录"。
|
||||
【正解给两条】①只读资源走 .qrc(昨天讲的资源系统,永不漂移);②可写数据用 applicationDirPath() 拼绝对路径。今晚作业验收标准里硬性要求用第②条。
|
||||
【过渡】"路径对了、文件读到了,结果一看内容——乱码。最后一个坑。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 讲授 13:30–16:00 · ★坑卡',
|
||||
'title': '★ 文本编码再遇(文件版)',
|
||||
'code': 'QTextStream ts(&file);\nts.setCodec("UTF-8"); // Qt 5 默认编码随平台/区域设置而变——跨机器必须显式指定',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'Day2 界面乱码(QString/源码编码)和今天文件乱码是同一根源的两次出现',
|
||||
'字节 ↔ 文字之间永远隔着一层「编码」,跨边界(源码/文件/网络)必须显式指定',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'Windows 记事本写的文件 Linux 程序读出乱码(或反过来)——不是「文件坏了」,是两边默认编码不同',
|
||||
]),
|
||||
],
|
||||
'minutes': 15, 'segment': '下午 · 编码坑',
|
||||
'notes': """【回指】开头直接回收 Day2 编码坑的"两个世界"图:字节世界和文字世界之间的那堵墙,今天换了一扇门,QTextStream。两次出现是刻意的间隔重复,目标是让"编码要显式指定"成为条件反射。
|
||||
【讲法】演示跨平台乱码(如有 Windows 文件样本最好,没有就用两个不同编码写读对照):QTextStream 不指定 codec 时默认随系统区域设置,同一份代码在两台机器读出不同结果,这种 bug 在交付现场最致命。
|
||||
【规则收束】第三条军规(黑板补完):"跨边界必声明编码"。源码用 UTF-8、文件读写显式 setCodec、网络同理。三天的编码主线在此收口。
|
||||
【过渡】"四个坑讲完,弹药齐了。今晚的作业是明天综合案例的前身。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 · 课堂作业 16:00–17:00(工程师答疑)',
|
||||
'title': '任务卡 2 · 本地数据存储小工具',
|
||||
'boxes': [
|
||||
('task', '任务卡 2', [
|
||||
'目标:设计「学员信息」结构(姓名/成绩/标签),支持增加记录并保存到 JSON 文件,重启程序后读出全部记录',
|
||||
'验收:至少存 2 条记录并在重启后完整读回;字段不丢失',
|
||||
'验收:数据文件路径用 applicationDirPath() 拼(今天的坑卡现学现用)',
|
||||
'参考:examples/json_io/ · p03/ch11/qfile_basic.cpp',
|
||||
]),
|
||||
],
|
||||
'minutes': 60, 'segment': '下午 · 作业',
|
||||
'notes': """【设计提示(开工前讲 5 分钟)】白板画结构体 Student{name, score, tag} 加两个函数 toJson/fromJson 互逆,和下午 JSON 课的 round-trip 判据一一对应。强调"读回来的字段名必须和写出去的完全一致",否则 round-trip 一测就露馅。
|
||||
【巡场检查点】对照今天四张坑卡逐个验收:①open() 判返回值;②ParseError 检查;③路径用 applicationDirPath() 不用裸相对路径;④QTextStream 显式 setCodec UTF-8。坑卡内容当天进验收,记忆效果最好。
|
||||
【分层】快的学员加"删除记录"或"按成绩排序",这直接是 Day5 综合案例的功能子集,今天做掉明天案例就轻松。
|
||||
【预告】今晚的作业就是明天综合案例的雏形,明天在此基础上加主窗口、表格、后台导入,就是一个完整小软件。"""
|
||||
},
|
||||
{
|
||||
'badge': '当日收尾',
|
||||
'title': '当日产出验证',
|
||||
'bullets': [
|
||||
'现场演示「写入 → 重启 → 读回」完整闭环',
|
||||
'讲得出 open()/ParseError 两处检查为什么不能省',
|
||||
'明天 Day5 全天综合项目案例:学员信息管理器——今晚作业就是它的雏形',
|
||||
],
|
||||
'minutes': 5, 'segment': '收尾',
|
||||
'notes': """【验收】抽 2 组演示完整闭环;口头追问 open 和 ParseError 两处检查的必要性,答得出才算过。
|
||||
【收线】"四天讲授到此结束。明天把你们这四天学的所有东西——窗口、控件、信号槽、文件、JSON、表格——组装成一个完整小软件,再补上最后一块拼图:多线程。"散场。"""
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,171 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Day5 · 综合项目案例讲解:学员信息管理器(全天)—— 页面对应 slides/day5.html。"""
|
||||
|
||||
SLIDES = [
|
||||
{
|
||||
'kind': 'title',
|
||||
'badge': 'C++/Qt 方向实训 · 综合案例日 · 明天起进入项目开发',
|
||||
'title': 'Day 5 · 综合项目案例讲解:学员信息管理器',
|
||||
'subtitle': '需求拆解 → 界面骨架 → 数据层 → 提升:QThread 防卡界面 → 流程规范',
|
||||
'bullets': [
|
||||
'上午:需求拆解 → 界面骨架(QMainWindow + QTableWidget + 对话框)→ 数据层(Student ↔ JSON)',
|
||||
'下午 1:提升——QThread 防卡界面(worker-object 模式)',
|
||||
'下午 2:开发流程与规范(Git/考核)+ 分组定项目方向',
|
||||
'Day6–9 学员项目开发;Day10 下午项目答辩',
|
||||
],
|
||||
'source': '参考实现:examples/student_manager/qt_student_manager.cpp(README)',
|
||||
'minutes': 5, 'segment': '开场',
|
||||
'notes': """【定位】今天不是新知识日,是"组装日",把四天学的全部东西捏成一个完整小软件,再补最后一块拼图(多线程)。开场明确这个定位,让学员从"听课模式"切换到"项目模式"。
|
||||
【讲法建议】全天以"先看成品→拆解→回指知识点"的节奏推进,每个环节都让学员意识到"这部分我已经学过",建立项目信心。教师参考实现 examples/student_manager/ 全程作为对照。
|
||||
【时长骨架】上午 1(需求加骨架)约 90 分钟;上午 2(数据层)约 90 分钟;下午 1(QThread 新授)约 70 分钟;下午 2(流程规范加分组)约 50 分钟。每段留 15 分钟缓冲。
|
||||
【红线提醒】下午 QThread 是全天最高优先级保底项。上午超时宁可砍需求拆解的深度也不能砍 QThread,否则项目期"界面冻结/跨线程崩溃"答疑成本数倍返还。"""
|
||||
},
|
||||
{
|
||||
'badge': '上午 1 · 需求拆解 → 界面骨架',
|
||||
'title': '需求拆解:先想清楚再动手',
|
||||
'boxes': [
|
||||
('concept', '核心概念——开发流程的第一步不是写代码', [
|
||||
'需求:管理学员记录(姓名/成绩/标签),支持添加、保存、读取、批量导入',
|
||||
'拆功能 → 映射到已学组件:列表=QTableWidget(Day4)、添加=模态对话框(Day3)、保存读取=QFile+JSON(Day4)、批量导入=后台线程(今天新授)、外壳=QMainWindow(Day3)',
|
||||
'拆完就得到任务清单——这正是 Day6 起你们在 Jira 上要做的事',
|
||||
]),
|
||||
],
|
||||
'minutes': 25, 'segment': '上午 · 需求拆解',
|
||||
'notes': """【地位】这一页是"从学生到工程师"思维转换的关键页:写代码之前先拆任务。中初级学员最常见的项目障碍不是"不会写"而是"不知道先写什么",本页就是治这个。
|
||||
【讲法】白板当堂拆:把"学员信息管理器"这个一句话需求,逐条拆成 5 个功能点,再把每个功能点映射到已学组件。每映射一条就问"这是哪天学的?"让全班齐声回答 Day2/3/4。这个回指动作能极大强化"我已经具备做项目的全部知识"的信心。
|
||||
【交付物】拆解的产物就是任务清单。明确告诉学员:"Day6 起你们在 Jira 上建的每一个 task,就应该长这样:一个动词开头、可验收、映射到具体技术点。"今天先有这个意识,明天分组就用得上。
|
||||
【过渡】"任务清单有了,开始拼第一块:界面骨架。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 1 · 需求拆解 → 界面骨架',
|
||||
'title': '界面骨架走查',
|
||||
'boxes': [
|
||||
('concept', '回指 Day3–4', [
|
||||
'QMainWindow:菜单(文件/记录/帮助)+ 工具栏 + 状态栏 + setCentralWidget(table)',
|
||||
'QAction 一个动作三处复用(菜单/工具栏/快捷键),每个 action 都要 connect',
|
||||
'添加记录:模态 exec() + QFormLayout + QDialogButtonBox',
|
||||
'表格 item 所有权:new 出来交给表格——父子所有权模型第 N 次出现',
|
||||
]),
|
||||
],
|
||||
'source': '现场跑:tools/run_qt.sh qt_student_manager(离屏自测:QT_QPA_PLATFORM=offscreen)',
|
||||
'minutes': 40, 'segment': '上午 · 骨架走查',
|
||||
'notes': """【讲法】自顶向下:先跑参考实现 examples/student_manager 给全班看成品长什么样,然后关掉投影,带学员从空 main() 开始一步步搭——QApplication → MainWindow(继承 QMainWindow) → setCentralWidget(table) → 菜单/工具栏/action → connect。每一步都问"这步对应哪天的哪个示例"。
|
||||
【回指密度】本页 4 条 bullet 每条都是一次回指:QMainWindow→Day3、QAction→Day3(坑卡"忘 connect")、模态对话框→Day3、表格 item 所有权→Day4 加贯穿全课程的所有权主线。回指的密度本身就是"项目=已学内容组装"的最佳论证。
|
||||
【代码组织建议】当场把 MainWindow 拆成 .h/.cpp,演示"声明在头、实现在 cpp"的工程结构,这是 Day6 起项目实作的基本功,今天第一次正式做。
|
||||
【互动】故意演示一次"建了 action 忘 connect",菜单项灰色点了没反应,让学员用 Day3 坑卡的语言定位问题。坑卡回炉一次。
|
||||
【过渡】"骨架立住了,往里灌数据。" """
|
||||
},
|
||||
{
|
||||
'badge': '上午 2 · 数据层',
|
||||
'title': '记录 ↔ JSON ↔ 磁盘',
|
||||
'code': 'struct Student { QString name; double score; QString tag; };\nQJsonObject toJson(const Student&); // 互逆的一对\nStudent fromJson(const QJsonObject&); // round-trip = 正确性标准',
|
||||
'boxes': [
|
||||
('concept', '回指 Day4(昨晚作业的完整版)', [
|
||||
'数据结构与界面分离:Student 结构体不知道 QTableWidget 的存在——Model/View 思想的朴素版',
|
||||
'健壮性三处检查全部在场:open() 返回值、QJsonParseError、路径用 applicationDirPath() 拼',
|
||||
]),
|
||||
('pitfall', '复盘昨晚作业的常见问题', [
|
||||
'保存和读取的字段名拼写不一致("score" vs "Score")——round-trip 一测就露馅',
|
||||
'这就是为什么验收标准是「写进去读出来一致」而不是「能生成文件」',
|
||||
]),
|
||||
],
|
||||
'minutes': 45, 'segment': '上午 · 数据层',
|
||||
'notes': """【讲法】把昨晚作业的数据层"升级"成案例版本:结构体 Student 加两个自由函数 toJson/fromJson。重点讲"数据结构与界面分离":Student 不知道 QTableWidget 的存在,这是 Model/View 思想的朴素实践(回指昨天表格页的钩子)。
|
||||
【当堂对照】让 1–2 位学员展示昨晚作业的数据层代码,对照参考实现讲差异,尤其是字段名拼写不一致的坑(昨晚作业的高发问题),round-trip 测试一跑就露馅,这正是"验收标准为什么是 round-trip"的活教材。
|
||||
【三处健壮性检查】打开参考实现源码,逐处指认 open() 判返回值、ParseError 检查、applicationDirPath() 拼路径,四天坑卡的成果在此集中体现。让学员看到"这些检查不是教条,是真实代码里必须有的"。
|
||||
【代码组织】toJson/fromJson 放在独立的 student.h(纯数据加序列化),不依赖 Qt Widgets,演示"按依赖方向分层"的工程习惯。
|
||||
【过渡】"功能齐了:能加、能存、能读。但有一个场景会让它冻住,下午第一项,新授。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 1 · 提升:多线程防卡界面(新授)',
|
||||
'title': '先看病症:把耗时活儿放主线程',
|
||||
'boxes': [
|
||||
('pitfall', '现场演示', [
|
||||
'把 msleep 循环直接放按钮槽里 → 整个窗口冻结:点不动、拖不动、系统提示「未响应」',
|
||||
'Day4 讲过:主线程阻塞 = 事件循环停摆——学员项目最高频的「卡死」来源',
|
||||
]),
|
||||
],
|
||||
'minutes': 15, 'segment': '下午 · QThread 新授',
|
||||
'notes': """【演示流程】在参考实现的"后台导入"按钮槽里,把 moveToThread 那套注释掉,直接在工作函数里 sleep 循环,跑给全班看:点导入后窗口完全冻结 5 秒,鼠标点哪都没反应,系统标题栏提示"未响应"。
|
||||
【讲法】让学员先用 Day4 的语言描述现象:"事件循环停摆了"。然后定性:"这是你们项目里最容易出现的卡死,根源不是代码错,而是把耗时活儿放错了线程。"
|
||||
【不急着给解法】演示完病症后停一下,问"你们觉得该怎么解决?"收集学员想法(多数会猜"开个线程"),自然引出下一页的 worker 模式。先有痛点再给方案,教学效果远好于直接讲语法。
|
||||
【过渡】"对,要开线程。但 Qt 开线程有个最容易被带歪的姿势,下一页讲正确做法。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 1 · 提升:多线程防卡界面(新授)',
|
||||
'title': '正解:worker-object 模式',
|
||||
'code': 'auto* thread = new QThread(this);\nauto* worker = new ImportWorker; // 不给 parent:有 parent 禁止 moveToThread\nworker->moveToThread(thread); // 必须在 start() 之前\nconnect(thread, &QThread::started, worker, [worker]{ worker->doImport(10); });\nconnect(worker, &ImportWorker::rowReady, this, /* 更新表格 */); // 自动 Queued\nthread->start();',
|
||||
'boxes': [
|
||||
('concept', '核心概念(三件套)', [
|
||||
'线程亲和性:每个 QObject 归属一个线程,GUI 控件必须归主线程',
|
||||
'worker 对象:官方推荐——耗时逻辑放 worker,moveToThread 搬进工作线程,而非继承 QThread 重写 run',
|
||||
'跨线程信号槽自动 Queued(Day2 连接方式闭环):worker 发信号、主线程槽更新表格,不需手动加锁',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'在工作线程直接操作 GUI 控件——未定义行为,跨线程崩溃头号来源',
|
||||
'moveToThread 写在 start() 之后 / worker 带了 parent——都不生效',
|
||||
]),
|
||||
],
|
||||
'source': '最小版:examples/qthread_worker/ · 案例集成版:student_manager 的 startImport()',
|
||||
'minutes': 40, 'segment': '下午 · QThread 新授',
|
||||
'notes': """【地位】这是全天最高优先级的新授点,也是四天来埋的"主线程阻塞"暗线的收口,必须讲透,时间是 40 分钟,不能压缩。
|
||||
【讲法顺序】先讲"不要怎么做":继承 QThread 重写 run 是网上最常见的写法但官方不推荐(容易把 worker 和线程生命周期搞混),明确否定它;再讲正解 worker-object 模式三件套:①worker 是普通 QObject 不给 parent;②moveToThread 搬家;③用信号槽通信。
|
||||
【三件套逐条讲】线程亲和性用"每个 QObject 有户籍"类比;worker 模式用"雇外援干活,干完打电话汇报"类比,打电话就是信号槽;跨线程信号槽自动 Queued 是 Qt 的魔法所在(回指 Day2 的 Direct/Queued/Auto,闭环)。强调"不需要手动加锁",这是 worker 模式相比 std::thread+mutex 的核心优势。
|
||||
【坑卡演示】在工作线程里直接 table->setItem,可能崩也可能"看起来正常"(又是 UB 双面性)。规则给死:"工作线程绝不碰 GUI,只发信号让主线程碰。"
|
||||
【闭环验证】参考实现的离屏自测会自动校验"导入 3 条后总数 5 条、界面未阻塞",跑一次让全班看绿色通过。
|
||||
【过渡】"最后一块拼图补上了,你们现在具备做一个完整 Qt 桌面软件的全部技能。下午最后一节,讲怎么把这些技能变成答辩成绩。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 2 · 开发流程与规范 + 分组定方向',
|
||||
'title': 'Git 提交习惯',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'每完成一个任务就提交,提交粒度对应任务粒度',
|
||||
'commit 信息说清「做了什么、为什么」——项目的可追溯记录,评审和回滚都靠它',
|
||||
]),
|
||||
('pitfall', None, [
|
||||
'攒几天代码一次性提交、信息写"update"——历史失去可追溯性,出问题无法定位是哪次改动引入',
|
||||
'从 Day6 第一次 commit 开始就按规范来',
|
||||
]),
|
||||
],
|
||||
'minutes': 15, 'segment': '下午 · 流程规范',
|
||||
'notes': """【讲法】用"代码是资产、commit 历史是资产的账本"类比,账本记得乱,资产就管不住。展示一个写得好的 commit 历史(每个消息动词开头、说清意图)和一个写得烂的(全是 update)的对比。
|
||||
【规范给具体可执行标准】①粒度:一个任务一次提交;②消息格式:动词开头("添加"/"修复"/"重构"),一行说做了什么,必要时正文说为什么;③不要提交无法编译的代码。
|
||||
【当堂练习(可选 10 分钟)】让每组在刚刚的案例代码上练习一次规范提交,这是明天起每天都要做的事,今天建立肌肉记忆。
|
||||
【过渡】"代码写得好只是考核的一部分。下一页看清考核的全貌。" """
|
||||
},
|
||||
{
|
||||
'badge': '下午 2 · 开发流程与规范 + 分组定方向',
|
||||
'title': '考核标准与分组定方向',
|
||||
'boxes': [
|
||||
('concept', None, [
|
||||
'考核不只看代码:项目管理(任务拆解/进度可见)+ 项目技术 + 职业素养(协作/答辩表达)',
|
||||
'只埋头写代码会丢掉其他维度的分',
|
||||
'今天散场前每组确认:项目选题 + 初步分工 + 任务拆解草稿(上午「需求拆解」那页就是模板)',
|
||||
]),
|
||||
('info', '兴趣钩子(各 5 分钟演示,选学)', [
|
||||
'动效:QPropertyAnimation(任何 Q_PROPERTY 都能被缓动曲线驱动)',
|
||||
'多状态 UI:QStateMachine(状态成为一等对象,替代一堆 bool 标志)',
|
||||
'线程的更高抽象:QtConcurrent / 线程池',
|
||||
]),
|
||||
],
|
||||
'minutes': 25, 'segment': '下午 · 考核 + 分组',
|
||||
'notes': """【考核讲法】投出考核标准三维度图,重点强调"技术只占三成",这是中初级学员最容易踩的认知坑(以为代码好就满分)。项目管理维度对应今天上午的"任务拆解",职业素养对应每天的协作和最后的答辩表达,把考核标准和今天讲的方法论一一对应。
|
||||
【分组实操】留 15 分钟让每组完成三件事:①确认项目选题(参考方案书备选项目,或自拟);②初步分工(谁主界面、谁主数据、谁主协调);③用今天上午"需求拆解"的格式写一版任务拆解草稿。教师巡场逐组确认,散场前每组交一份。
|
||||
【钩子讲法】三个钩子各 5 分钟演示即可,目的有二:①给学有余力的组指明加分方向(动画/状态机/QtConcurrent 都是答辩亮点);②告诉所有人"Qt 还有很多没讲的能力,项目期可以按需自学",留个引子。
|
||||
【收尾承诺】明确告诉学员:明天起进入项目实作,工程师全程在场答疑;今天案例的全部代码(examples/student_manager/)对他们开放作参考。"""
|
||||
},
|
||||
{
|
||||
'badge': '当日收尾 · 进入项目期',
|
||||
'title': '当日产出验证 · 进入项目期',
|
||||
'code': 'QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_student_manager\n# 自测 1/2:JSON 写入 → 清空 → 读回,2 条记录完整\n# 自测 2/2:后台线程导入 3 条,总计 5 条,界面未阻塞',
|
||||
'bullets': [
|
||||
'每组提交:项目选题 + 分工说明 + 任务拆解草稿',
|
||||
'明天 Day6 起:学员编码 + 工程师指导',
|
||||
'Day10 下午:项目答辩',
|
||||
],
|
||||
'minutes': 5, 'segment': '收尾',
|
||||
'notes': """【验收】跑参考实现的离屏自测,全班看两行绿色通过;每组当场提交三件套(选题/分工/拆解草稿)。
|
||||
【结训语】"五天前你们刚进门,今天你们已经能独立做一个会存数据、不卡界面、有规范流程的桌面软件。后面五天,把它变成你们自己的项目。Day10 答辩见。"散场。"""
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""教学幻灯片 PPT 生成器(4 天讲授 + Day5 综合案例)—— 手工维护,非 wiki 生成产物。
|
||||
|
||||
- 数据:同目录 day1.py … day5.py(每页一个 dict)
|
||||
- 产物:docs/teaching/ppt/day{1..5}.pptx,页面内容与 docs/teaching/slides/*.html 对应
|
||||
- 讲解说明文字(含建议时长/讲法/演示/互动/过渡)只写入**演讲者备注**,不进主页面
|
||||
|
||||
用法(python-pptx 不在系统 Python 里,用任一装了它的解释器):
|
||||
python3 -m venv ~/.venv-pptx && ~/.venv-pptx/bin/pip install python-pptx
|
||||
~/.venv-pptx/bin/python tools/teaching_ppt/gen.py
|
||||
"""
|
||||
import importlib
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pptx import Presentation
|
||||
from pptx.dml.color import RGBColor
|
||||
from pptx.enum.text import MSO_ANCHOR
|
||||
from pptx.oxml.ns import qn
|
||||
from pptx.util import Inches, Pt
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
OUT_DIR = os.path.join(ROOT, 'docs', 'teaching', 'ppt')
|
||||
|
||||
FONT = 'Microsoft YaHei'
|
||||
MONO = 'Consolas'
|
||||
|
||||
# (填充, 边框, 标题色)
|
||||
BOX_STYLE = {
|
||||
'concept': (RGBColor(0xE8, 0xF5, 0xE9), RGBColor(0x2F, 0x85, 0x5A), RGBColor(0x22, 0x54, 0x3D)),
|
||||
'pitfall': (RGBColor(0xFD, 0xEC, 0xEA), RGBColor(0xC5, 0x30, 0x30), RGBColor(0x82, 0x27, 0x27)),
|
||||
'info': (RGBColor(0xEB, 0xF8, 0xFF), RGBColor(0x2B, 0x6C, 0xB0), RGBColor(0x2A, 0x43, 0x65)),
|
||||
'task': (RGBColor(0xFF, 0xFA, 0xEB), RGBColor(0xB7, 0x79, 0x1F), RGBColor(0x74, 0x4D, 0x0B)),
|
||||
}
|
||||
BOX_DEFAULT_HEADING = {'concept': '核心概念', 'pitfall': '常见的坑', 'info': '说明', 'task': '任务卡'}
|
||||
|
||||
TEXT_DARK = RGBColor(0x1A, 0x20, 0x2C)
|
||||
TEXT_GRAY = RGBColor(0x71, 0x80, 0x96)
|
||||
CODE_BG = RGBColor(0x1E, 0x1E, 0x1E)
|
||||
CODE_FG = RGBColor(0xD4, 0xD4, 0xD4)
|
||||
|
||||
ML = 0.45 # 左右边距 (inch)
|
||||
W = 13.333 - 2 * ML
|
||||
|
||||
|
||||
def set_font(run, name=FONT, size=None, bold=None, color=None, italic=None):
|
||||
f = run.font
|
||||
f.name = name
|
||||
if size is not None:
|
||||
f.size = Pt(size)
|
||||
if bold is not None:
|
||||
f.bold = bold
|
||||
if italic is not None:
|
||||
f.italic = italic
|
||||
if color is not None:
|
||||
f.color.rgb = color
|
||||
# 东亚字体要单独设置(python-pptx 的 .name 只设 latin)
|
||||
rpr = run._r.get_or_add_rPr()
|
||||
ea = rpr.find(qn('a:ea'))
|
||||
if ea is None:
|
||||
ea = rpr.makeelement(qn('a:ea'), {})
|
||||
rpr.append(ea)
|
||||
ea.set('typeface', name)
|
||||
|
||||
|
||||
def add_textbox(slide, left, top, width, height):
|
||||
tb = slide.shapes.add_textbox(Inches(left), Inches(top), Inches(width), Inches(height))
|
||||
tf = tb.text_frame
|
||||
tf.word_wrap = True
|
||||
tf.margin_left = tf.margin_right = Pt(4)
|
||||
tf.margin_top = tf.margin_bottom = Pt(2)
|
||||
return tb, tf
|
||||
|
||||
|
||||
def wrap_lines(text, per_line):
|
||||
return max(1, math.ceil(len(text) / per_line))
|
||||
|
||||
|
||||
def add_box(slide, kind, heading, lines, top):
|
||||
"""彩色概念/坑/说明卡,返回其高度(inch)。"""
|
||||
fill, border, head_color = BOX_STYLE[kind]
|
||||
body_h = sum(wrap_lines(t, 62) for t in lines) * 0.28
|
||||
h = 0.42 + body_h + 0.10
|
||||
shape = slide.shapes.add_shape(1, Inches(ML), Inches(top), Inches(W), Inches(h)) # 1 = 矩形
|
||||
shape.fill.solid()
|
||||
shape.fill.fore_color.rgb = fill
|
||||
shape.line.color.rgb = border
|
||||
shape.line.width = Pt(1.2)
|
||||
shape.shadow.inherit = False
|
||||
tf = shape.text_frame
|
||||
tf.word_wrap = True
|
||||
tf.margin_left = Pt(10)
|
||||
tf.margin_right = Pt(10)
|
||||
tf.margin_top = Pt(5)
|
||||
tf.margin_bottom = Pt(4)
|
||||
tf.vertical_anchor = MSO_ANCHOR.TOP
|
||||
p = tf.paragraphs[0]
|
||||
r = p.add_run()
|
||||
r.text = heading or BOX_DEFAULT_HEADING[kind]
|
||||
set_font(r, size=14, bold=True, color=head_color)
|
||||
for line in lines:
|
||||
p = tf.add_paragraph()
|
||||
p.space_before = Pt(3)
|
||||
r = p.add_run()
|
||||
r.text = '• ' + line
|
||||
set_font(r, size=13, color=TEXT_DARK)
|
||||
return h
|
||||
|
||||
|
||||
def add_code(slide, code, top):
|
||||
lines = code.rstrip('\n').split('\n')
|
||||
h = 0.26 + len(lines) * 0.245
|
||||
shape = slide.shapes.add_shape(1, Inches(ML), Inches(top), Inches(W), Inches(h))
|
||||
shape.fill.solid()
|
||||
shape.fill.fore_color.rgb = CODE_BG
|
||||
shape.line.fill.background()
|
||||
shape.shadow.inherit = False
|
||||
tf = shape.text_frame
|
||||
tf.word_wrap = False
|
||||
tf.margin_left = Pt(12)
|
||||
tf.margin_top = Pt(6)
|
||||
first = True
|
||||
for line in lines:
|
||||
p = tf.paragraphs[0] if first else tf.add_paragraph()
|
||||
first = False
|
||||
r = p.add_run()
|
||||
r.text = line if line else ' '
|
||||
set_font(r, name=MONO, size=12, color=CODE_FG)
|
||||
return h
|
||||
|
||||
|
||||
def set_notes(slide, spec):
|
||||
tf = slide.notes_slide.notes_text_frame
|
||||
tf.clear()
|
||||
minutes = spec.get('minutes')
|
||||
segment = spec.get('segment', '')
|
||||
header = f"⏱ 建议 {minutes} 分钟 · {segment}" if minutes else segment
|
||||
paras = ([header] if header.strip(' ·') else []) + spec.get('notes', '').strip().split('\n')
|
||||
first = True
|
||||
for para in paras:
|
||||
p = tf.paragraphs[0] if first else tf.add_paragraph()
|
||||
first = False
|
||||
r = p.add_run()
|
||||
r.text = para
|
||||
set_font(r, size=12, bold=(first and bool(minutes)))
|
||||
|
||||
|
||||
def build_title_slide(prs, spec):
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
_, tf = add_textbox(slide, ML, 0.35, W, 0.35)
|
||||
r = tf.paragraphs[0].add_run()
|
||||
r.text = spec.get('badge', '')
|
||||
set_font(r, size=12, color=TEXT_GRAY)
|
||||
_, tf = add_textbox(slide, ML, 1.5, W, 1.0)
|
||||
r = tf.paragraphs[0].add_run()
|
||||
r.text = spec['title']
|
||||
set_font(r, size=40, bold=True, color=TEXT_DARK)
|
||||
_, tf = add_textbox(slide, ML, 2.6, W, 0.6)
|
||||
r = tf.paragraphs[0].add_run()
|
||||
r.text = spec.get('subtitle', '')
|
||||
set_font(r, size=20, color=RGBColor(0x2B, 0x6C, 0xB0))
|
||||
top = 3.5
|
||||
_, tf = add_textbox(slide, ML, top, W, 2.6)
|
||||
first = True
|
||||
for b in spec.get('bullets', []):
|
||||
p = tf.paragraphs[0] if first else tf.add_paragraph()
|
||||
first = False
|
||||
p.space_before = Pt(6)
|
||||
r = p.add_run()
|
||||
r.text = '· ' + b
|
||||
set_font(r, size=16, color=TEXT_DARK)
|
||||
if spec.get('source'):
|
||||
_, tf = add_textbox(slide, ML, 6.95, W, 0.4)
|
||||
r = tf.paragraphs[0].add_run()
|
||||
r.text = spec['source']
|
||||
set_font(r, size=11, color=TEXT_GRAY)
|
||||
set_notes(slide, spec)
|
||||
|
||||
|
||||
def build_content_slide(prs, spec, deck, idx):
|
||||
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
||||
_, tf = add_textbox(slide, ML, 0.18, W, 0.32)
|
||||
r = tf.paragraphs[0].add_run()
|
||||
r.text = spec.get('badge', '')
|
||||
set_font(r, size=11, color=TEXT_GRAY)
|
||||
_, tf = add_textbox(slide, ML, 0.5, W, 0.75)
|
||||
r = tf.paragraphs[0].add_run()
|
||||
r.text = spec['title']
|
||||
set_font(r, size=27, bold=True, color=TEXT_DARK)
|
||||
top = 1.35
|
||||
for b in spec.get('bullets', []):
|
||||
_, tf = add_textbox(slide, ML, top, W, 0.4)
|
||||
r = tf.paragraphs[0].add_run()
|
||||
r.text = '· ' + b
|
||||
set_font(r, size=15, color=TEXT_DARK)
|
||||
top += wrap_lines(b, 56) * 0.34 + 0.04
|
||||
if spec.get('code'):
|
||||
top += 0.06
|
||||
top += add_code(slide, spec['code'], top) + 0.14
|
||||
for box in spec.get('boxes', []):
|
||||
kind, heading, lines = box
|
||||
top += add_box(slide, kind, heading, lines, top) + 0.14
|
||||
if spec.get('source'):
|
||||
_, tf = add_textbox(slide, ML, 7.02, W, 0.35)
|
||||
r = tf.paragraphs[0].add_run()
|
||||
r.text = spec['source']
|
||||
set_font(r, size=10.5, color=TEXT_GRAY)
|
||||
if top > 7.05:
|
||||
print(f' [overflow] {deck} 第{idx}页「{spec["title"]}」内容高度 {top:.2f}in > 7.05in,请精简')
|
||||
set_notes(slide, spec)
|
||||
|
||||
|
||||
def build_deck(mod_name):
|
||||
mod = importlib.import_module(mod_name)
|
||||
prs = Presentation()
|
||||
prs.slide_width = Inches(13.333)
|
||||
prs.slide_height = Inches(7.5)
|
||||
for i, spec in enumerate(mod.SLIDES, 1):
|
||||
if spec.get('kind') == 'title':
|
||||
build_title_slide(prs, spec)
|
||||
else:
|
||||
build_content_slide(prs, spec, mod_name, i)
|
||||
out = os.path.join(OUT_DIR, f'{mod_name}.pptx')
|
||||
prs.save(out)
|
||||
total = sum(float(s.get('minutes', 0) or 0) for s in mod.SLIDES)
|
||||
print(f'{out}: {len(mod.SLIDES)} 页, 备注时长合计 ≈ {total:.0f} 分钟')
|
||||
|
||||
|
||||
def main():
|
||||
os.makedirs(OUT_DIR, exist_ok=True)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
for n in range(1, 6):
|
||||
build_deck(f'day{n}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user