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 即可幂等重生成
231 lines
18 KiB
Python
231 lines
18 KiB
Python
# -*- 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、表格——组装成一个完整小软件,再补上最后一块拼图:多线程。"散场。"""
|
||
},
|
||
]
|