ea6f12e7ac
- 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 即可幂等重生成
260 lines
20 KiB
Python
260 lines
20 KiB
Python
# -*- 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 程序;明天给它一个正经软件的骨架。"散场。"""
|
||
},
|
||
]
|