Files
scuc-qt-course/tools/teaching_ppt/day2.py
T
张宗平 ea6f12e7ac 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 即可幂等重生成
2026-07-05 16:08:13 +08:00

260 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Day2 · Qt 入门 / 信号槽 / 计算器案例 —— 页面对应 slides/day2.html。"""
SLIDES = [
{
'kind': 'title',
'badge': 'C++/Qt 方向实训 · 第 2 / 4 个讲授日',
'title': 'Day 2 · Qt 入门 / 信号槽 → 计算器',
'subtitle': 'Qt 概述 / 创建项目 / 第一个程序 / 信号槽机制 / 案例:简易计算器',
'bullets': [
'上午 08:3011: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:3011: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:3011: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:3011:00',
'title': '第一个 Qt 小程序',
'boxes': [
('concept', None, [
'QApplication 是唯一的事件循环拥有者,app.exec() 阻塞进入事件循环分发系统事件',
'Q_OBJECT 宏触发 moc 生成信号槽/属性系统所需的元对象代码',
]),
('pitfall', None, [
'一个进程只能有一个 QApplication;忘 exec() 窗口一闪而过',
'用了信号槽却忘 Q_OBJECTundefined 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:3011: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/labelsdelete 根,全树倒序析构。强调工程收益:界面上百个控件,不写一个 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:3016:00',
'title': '信号槽:内置 / 自定义 / lambda',
'boxes': [
('concept', None, [
'观察者模式的 Qt 实现:发送者只 emit,不知道谁在听——对象间彻底解耦',
'新语法 &Class::signal 编译期检查;旧字符串语法 SIGNAL()/SLOT() 运行时检查',
'连接方式 Direct/Queued/AutoDay5 跨线程时回收闭环)',
]),
('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:3016: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 生成了什么代码',
'明天 Day3QMainWindow / QDialog / 布局讲透 / 常用控件 / QSS——计算器升级成「正经软件」',
],
'minutes': 5, 'segment': '收尾',
'notes': """【验收】抽 2–3 人现场演示计算器运算;全班自查中文无乱码。
【钩子讲法】当堂打开一个 moc_*.cpp 滚一屏,不逐行讲,只说一句:"信号槽没有魔法,全在这份生成代码里,感兴趣的回去读。"能看懂的学员会对元对象系统祛魅。
【收线】"今天你们写出了第一个有交互的 Qt 程序;明天给它一个正经软件的骨架。"散场。"""
},
]