Files
scuc-qt-course/tools/teaching_ppt/day4.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

231 lines
18 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 -*-
"""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:3016: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:3011:00',
'title': '事件循环与派发:事件 vs 信号',
'boxes': [
('concept', None, [
'Day2 的 app.exec() 拆开看:事件循环不停从队列取 QEvent 派发给目标控件',
'事件 = 框架推给控件的通知(鼠标/键盘/重绘);信号槽 = 对象间主动通知',
'两套机制并存不冲突',
]),
('pitfall', '明天的伏笔', [
'主线程做耗时操作 = 事件循环停摆 = 整个界面冻结',
'学员项目最高频的「卡死」来源,正确解法(多线程)明天综合案例讲',
]),
],
'source': '出处:p03/ch02/minimal_qt_app.cppexec',
'minutes': 15, 'segment': '上午 · 事件循环',
'notes': """【衔接】回指 Day2"事件循环=前台服务员"的类比,今天把它拆开:服务员从队列里取单子(QEvent)派给对应窗口。然后区分两套机制:事件是"系统/框架主动通知控件",信号槽是"对象之间互相通知",同一个按钮点击会同时产生鼠标事件和 clicked 信号,两者不冲突。
【坑卡只埋不拆】主线程阻塞坑今天只演示现象不解决:在按钮槽里塞一个 sleep(5) 的循环跑给学员看,窗口冻住、拖不动、系统提示未响应。明确说"这是明天综合案例的核心提升点,今天的任务是认清病症"。埋点到位,明天 QThread 出场时学员会有"终于等到"的投入感。
【过渡】"事件派给控件之后,控件怎么处理?两条路。" """
},
{
'badge': '上午 · 讲授 08:3011: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:3011: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:3011:00',
'title': '表格:QTableWidget 与单元格事件',
'boxes': [
('concept', None, [
'item 类控件:数据直接塞进控件(QTableWidgetItemnew 出来交给表格接管所有权)',
'"表格事件"以信号暴露:cellClicked / cellDoubleClicked / itemChanged',
]),
('info', '钩子 · 背后是完整的 Model/View 架构', [
'数据(model)与展示(view)彻底分离、同一模型可喂多个视图',
'改模型必须发 dataChanged 等信号视图才刷新——数据规模大/多视图联动时的正解',
'自学入口:examples/model_view/',
]),
],
'source': '出处:examples/table_widget/qt_table_widget_basics.cppREADME',
'minutes': 20, 'segment': '上午 · 表格',
'notes': """【讲法】跑 table_widget 示例:填充 3×3 加点击单元格看状态栏变化加双击编辑触发 itemChanged。重点讲 QTableWidgetItem 的所有权,又是 new 出来交给控件(QObject 父子所有权的第 N 次出现,点一句即可不再展开)。
【两条路线的边界】QTableWidgetitem 类)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:3016: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:3016: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.cppREADME',
'minutes': 25, 'segment': '下午 · JSON',
'notes': """【讲法】round-trip 是本页核心:用"打包寄出再拆包"类比序列化/反序列化,正确性判据不是"能生成文件"而是"写出去读回来字段一个不丢"。这个判据今晚作业的验收标准就是它。
【四件套】画树状结构讲清层级:Document 包 Object/ArrayObject 是键值对,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、表格——组装成一个完整小软件,再补上最后一块拼图:多线程。"散场。"""
},
]