# -*- 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 程序；明天给它一个正经软件的骨架。"散场。"""
},
]
