# -*- 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、表格——组装成一个完整小软件，再补上最后一块拼图：多线程。"散场。"""
},
]
