# -*- coding: utf-8 -*-
"""Day3 · QMainWindow / QDialog / 布局与控件 / QSS —— 页面对应 slides/day3.html。"""

SLIDES = [
{
 'kind': 'title',
 'badge': 'C++/Qt 方向实训 · 第 3 / 4 个讲授日',
 'title': 'Day 3 · QMainWindow / QDialog / 布局与控件',
 'subtitle': '主窗口骨架 / 对话框交互 / 布局讲透 / 常用控件 / QSS 美化',
 'bullets': [
   '上午 08:30–11:00 讲授：QMainWindow · 对话框 QDialog · 标准对话框',
   '上午 11:00–12:00 课堂练习：计算器套上 QMainWindow（菜单 + 停靠窗）',
   '下午 13:30–16:00 讲授：布局管理器（回收脚手架）· 常用控件与 .qrc · QSS',
   '下午 16:00–17:00 作业：QSS 美化计算器 + 缩放自适应',
 ],
 'minutes': 3, 'segment': '开场',
 'notes': """【主线】今天全天围绕一件事：把昨天的计算器升级成"正经软件"。上午给骨架（主窗口加对话框），下午管排版和外观。开场就展示教师版的"升级后计算器"成品，让学员看到今天结束时自己的程序长什么样，目标可视化。
【时长骨架】上午：QMainWindow 30 → QDialog 模态非模态 30 → 标准对话框 15，讲授约 75 分钟，剩余给随堂跟敲和答疑。下午：布局 30 → 控件与 qrc 25 → QSS 25，随堂练穿插。
【衔接】昨天的"堆上建控件"军规今天在对话框一节会第三次出现，留意把这条线讲成呼应而不是重复。"""
},
{
 'badge': '上午 · 讲授 08:30–11:00',
 'title': 'QMainWindow：标准桌面软件的骨架',
 'boxes': [
   ('concept', None, [
     'QMainWindow 不是「大号 QWidget」：菜单栏/工具栏/状态栏/停靠窗各就其位的固定区域协议',
     '中心部件必须 setCentralWidget() 显式安放',
     '菜单项和工具栏按钮的统一抽象是 QAction——一个动作，多处复用',
   ]),
   ('pitfall', None, [
     '不设中心部件直接摆控件——布局行为不可预期',
     'QAction 显示出来 ≠ 生效：忘 connect 就是「菜单点了没反应」',
   ]),
 ],
 'source': '出处：p03/ch05/central_widget.cpp · dock_widget.cpp',
 'minutes': 30, 'segment': '上午 · QMainWindow',
 'notes': """【引入】投一张学员都认识的软件截图（VS Code / WPS），指认五个区域：菜单/工具/状态/中心/停靠。"QMainWindow 就是这套行业标准布局的现成实现。"
【讲法】跑 central_widget.cpp 与 dock_widget.cpp，边跑边对照代码：setCentralWidget、menuBar()->addMenu、addToolBar、statusBar()->showMessage 四个入口各演示一次。
【QAction 重点】用"一个动作三个入口"讲：同一个 QAction 同时挂菜单和工具栏，禁用它两处一起变灰，这就是抽象的价值。坑卡里"菜单点了没反应"发生率极高，让学员现在就形成"建 action 后第一件事 connect"的顺序习惯。
【互动】问："中心部件只能有一个，那复杂界面怎么办？"答案是中心部件本身可以是任意组合控件（下午布局课解决），先埋问题。
【过渡】"主窗口是常驻界面；临时冒出来问一句的界面叫对话框。" """
},
{
 'badge': '上午 · 讲授 08:30–11:00',
 'title': 'QDialog：模态与非模态',
 'boxes': [
   ('concept', None, [
     '模态 exec()：阻塞等结果，返回值就是用户的选择（Accepted/Rejected）',
     '非模态 show()：立即返回，窗口独立存活——对象必须活过当前函数作用域',
     '非模态三种写法：堆 + parent / WA_DeleteOnClose 自清理 /（反例）栈',
   ]),
   ('pitfall', '常见的坑（所有权军规第三次出现！）', [
     '非模态对话框建在栈上：show() 完函数一返回就析构——窗口一闪而过',
     'p03/ch06 的三个 modeless 版本就是这个坑的对照教材',
   ]),
 ],
 'source': '出处：p03/ch06/modal_dialog.cpp · modeless_dialog_stack.cpp · _heap.cpp · _deleteonclose.cpp',
 'minutes': 30, 'segment': '上午 · QDialog',
 'notes': """【讲法】模态/非模态先用生活类比定性：模态=收银台付款（不办完不能干别的），非模态=旁边开着的计算器（随时切换）。然后讲代码差异的本质：exec() 内部自己转事件循环所以能阻塞，show() 只是显示。
【三连跑对照】依次跑 ch06 三个 modeless 文件：栈版一闪而过（学员先猜原因，Day2 军规该有人能答上）、堆版正常但要考虑谁删、DeleteOnClose 版关窗自清理。同一功能三种生命周期策略并排，是全仓库最好的所有权教材。
【收束】黑板军规更新："给了 parent 的控件必须 new"加"非模态对话框必须活过函数"。本质是同一条：对象的生命周期必须覆盖它的使用周期。
【过渡】"自己写对话框之前，先看看 Qt 送了哪些现成的。" """
},
{
 'badge': '上午 · 讲授 08:30–11:00',
 'title': '标准对话框速览',
 'code': 'auto ret = QMessageBox::question(this, "确认", "真的要删除吗？");\nif (ret == QMessageBox::Yes) { /* ... */ }\n\nQString path = QFileDialog::getOpenFileName(this, "选择文件", "", "*.json");\nif (!path.isEmpty()) { /* ... */ }   // 用户可能点取消：必须处理空返回',
 'boxes': [
   ('pitfall', None, [
     '不处理「用户点了取消」（空路径/Rejected）就直接往下用——健壮性问题高发点',
   ]),
 ],
 'source': '出处：p03/ch06/message_box_question.cpp · message_box_custom.cpp · file_dialog.cpp',
 'minutes': 15, 'segment': '上午 · 标准对话框',
 'notes': """【讲法】两个静态函数各演示一次即可，重点全放在坑卡上：让学员齐声回答"getOpenFileName 之后第一行代码写什么？"判空。用户永远可能点取消，这是健壮性思维的第一课（Day4 的 open() 判返回值、ParseError 检查是同一条思维线）。
【提示】QMessageBox 的 question/information/warning/critical 四兄弟提一嘴图标差异即可，不逐个演示。
【过渡】"骨架和对话框都有了。上午最后一小时，把你的计算器装进这副骨架。" """
},
{
 'badge': '上午 · 课堂练习 11:00–12:00（工程师指导、答疑）',
 'title': '任务卡 1 · 给计算器套上 QMainWindow',
 'boxes': [
   ('task', '任务卡 1', [
     '目标：昨天的计算器改造为 QMainWindow 应用；加菜单「帮助→关于」弹 QMessageBox；加「历史」停靠窗（可先放空 QLabel）',
     '验收：计算器功能不回退；菜单动作有响应；停靠窗可拖动/收起',
     '参考：p03/ch05/central_widget.cpp · dock_widget.cpp · p03/ch06/message_box_question.cpp',
   ]),
 ],
 'minutes': 60, 'segment': '上午 · 课堂练习',
 'notes': """【关键改造思路（先讲 3 分钟再放手）】昨天的计算器是一个 QWidget，今天不重写，把它整个 setCentralWidget 进 QMainWindow。这个"旧组件装进新骨架"的动作本身就是组件化思维的第一次实践，点破它。
【巡场检查点】①菜单 QAction 忘 connect（上午坑卡现场版，让学员自己对照坑卡定位）；②功能回退（改造弄坏了昨天的计算逻辑，借机讲"改结构不改逻辑"的重构纪律）。
【分层】快的学员让"历史"停靠窗真的记录每次运算（QLabel 追加文本即可），这直接为 Day5 案例的表格记录做铺垫。
【收尾】抽 1–2 人演示：点菜单弹关于、拖动停靠窗。"""
},
{
 'badge': '下午 · 讲授 13:30–16:00',
 'title': '布局管理器（回收昨天的脚手架）',
 'boxes': [
   ('concept', None, [
     '布局接管子控件位置/大小计算，随窗口缩放自动重排',
     'QHBox / QVBox / QGrid 三兄弟 + 嵌套组合 + 伸缩因子 addStretch()',
     '布局对象同样遵循 QObject 父子所有权',
   ]),
   ('pitfall', None, [
     '同一控件不能属于两个布局',
     '布局设置后又用 setGeometry()/move() 手动改位置——被下次刷新覆盖，看起来「设置没生效」',
   ]),
 ],
 'source': '出处：p03/ch08/custom_widget.cpp',
 'minutes': 30, 'segment': '下午 · 布局',
 'notes': """【兑现承诺】开场明确"昨天欠的布局课现在还"：把计算器脚手架里的 QGridLayout 拿出来讲原理。为什么 addWidget(w, row, col) 之后拉伸窗口按钮会跟着变？因为布局在每次 resize 时重算所有子控件的几何。
【讲法】演示三兄弟各一个最小例子，然后重点讲嵌套：垂直布局套水平布局，拼出"上显示屏下键盘"的计算器结构。学员昨天已经无意识用过，今天补上心智模型。addStretch 用"弹簧"类比。
【坑卡】当堂演示"布局后 move() 失效"：move 完拉伸一下窗口，控件跳回布局位置，"看起来没生效"的真相是被布局覆盖。规则：进了布局就别手动定位，二选一。
【随堂练 10 分钟】把自己计算器的布局改成"显示屏占 1 份、键盘占 3 份"的比例（伸缩因子）。
【过渡】"位置管好了，看控件本身还有什么花样。" """
},
{
 'badge': '下午 · 讲授 13:30–16:00',
 'title': '常用控件 + 资源系统 .qrc',
 'boxes': [
   ('concept', None, [
     'QLabel 一个控件多种形态：纯文本 / 富文本 HTML / 图片 QPixmap / 动图 QMovie',
     '输入靠 QLineEdit；按钮族 QPushButton / QRadioButton / QCheckBox',
     ':/... 路径是 Qt 资源系统：资源编译期打包进可执行文件，发布不用带散图',
   ]),
   ('pitfall', None, [
     '.qrc 路径与代码引用不一致：运行时静默失败（图不显示），不报编译错误',
     '忘接 CMake 的 AUTORCC / qt5_add_resources——资源系统不生效',
   ]),
 ],
 'source': '出处：p03/ch08/label_text_html.cpp · label_pixmap.cpp · label_movie.cpp',
 'minutes': 25, 'segment': '下午 · 控件与资源',
 'notes': """【讲法】三个 label 示例连跑（文本→HTML→图片/动图），节奏快一点，每个 5 分钟：控件 API 本身不难，难的是"知道有什么可用"，本页目标是建目录索引不是背 API。
【qrc 重点】打开 label_pixmap.qrc 和对应 cpp 对照讲：qrc 里登记路径 → 代码里 :/ 引用 → CMake AUTORCC 编译打包。坑卡演示：把代码里的 :/ 路径改错一个字母再跑，不报错、图消失，"静默失败"要亲眼见过才会警惕（Day4 的 open()/ParseError 静默失败是同一主题）。
【互动】问："图片放资源系统 vs 放程序旁边的目录，各适合什么？"只读小资源进 qrc；用户数据/大文件放文件系统（Day4 的主题，顺势预告）。
【过渡】"功能齐了，长得还不行。最后一节，化妆。" """
},
{
 'badge': '下午 · 讲授 13:30–16:00',
 'title': 'QSS 界面美化',
 'code': 'QPushButton#okButton { background-color: #2f855a; border-radius: 6px; }\nQPushButton#okButton:hover { background-color: #276749; }',
 'boxes': [
   ('concept', None, [
     '语法脱胎于 CSS：类型选择器 / ID 选择器（#objectName）/ 伪状态（:hover :pressed）+ 层叠',
     '作用对象是 QWidget 树而非 DOM；「长什么样」与「做什么」分离，改样式不碰逻辑',
   ]),
   ('pitfall', None, [
     'ID 选择器必须先 setObjectName()',
     'setStyleSheet() 级联影响所有子控件——范围失控最易踩，样式尽量写窄（带选择器）',
   ]),
 ],
 'source': '出处：examples/qss_styling/qt_qss_styling.cpp（README 有练习方向）',
 'minutes': 25, 'segment': '下午 · QSS',
 'notes': """【讲法】跑 qss_styling 示例，然后当堂改样式立即重跑，"改外观不碰逻辑"的分离感是本节要传达的核心体验。学过 CSS 的学员点一句"就是 CSS 的方言"，没学过的按"选择器=谁、属性=怎么画"从零讲。
【坑卡演示】级联失控：在顶层 widget 上裸写 background-color（不带选择器），跑起来所有子控件全变色，然后加上类型选择器收窄。让学员记"样式写窄"三个字。
【伪状态】:hover/:pressed 是"零代码交互反馈"，性价比极高，今晚作业的验收点之一。
【随堂练 15 分钟】给上午的主窗口版计算器套第一版 QSS（哪怕只改按钮底色），作业时段继续精修。
【过渡】"今晚作业就是把它做完做好，验收标准在下一页。" """
},
{
 'badge': '下午 · 课堂作业 16:00–17:00（工程师答疑）',
 'title': '任务卡 2 · 用 QSS 美化计算器',
 'boxes': [
   ('task', '任务卡 2', [
     '目标：数字键/运算键/等号键用 QSS 区分颜色；整体布局随窗口缩放自适应',
     '验收：至少 2 类按钮不同 background-color；至少 1 个按钮有 :hover 或 :pressed',
     '验收：拉伸窗口按钮跟着重排',
     '参考：examples/qss_styling/ · p03/ch08/custom_widget.cpp',
   ]),
 ],
 'minutes': 60, 'segment': '下午 · 作业',
 'notes': """【实现提示（开工前讲 3 分钟）】三类按键区分的正确姿势：按类别 setObjectName（或用动态属性），QSS 里 ID/属性选择器分别上色。不要给每个按钮单独 setStyleSheet（层级混乱且难维护）。
【巡场检查点】①样式写太宽把显示屏也染色了（下午坑卡现场版）；②hover 无效，大概率 objectName 没设或拼错，静默失败，让学员自己按坑卡排查。
【分层】快的学员挑战"深色主题"整套配色，或给等号键加 :pressed 按下变暗的两态反馈。
【预告】明天计算器线暂停一天（事件/绘图/文件），Day5 综合案例会用到今天所有的骨架加布局加美化技能。"""
},
{
 'badge': '当日收尾',
 'title': '当日产出验证',
 'bullets': [
   '现场演示：主窗口版计算器（菜单/停靠窗）+ QSS 配色 + 缩放自适应，功能不回退',
   '钩子：QSS 与前端 CSS 同源；Qt Designer / QML 是另一条 UI 路线（知道存在即可）',
   '明天 Day4：事件机制 / 绘图 / 表格 + 文件系统——数据要开始落盘了',
 ],
 'minutes': 5, 'segment': '收尾',
 'notes': """【验收】抽 2–3 组演示升级后的计算器，其他人对照验收标准自查打钩。
【钩子讲法】一句话安放 Designer/QML："拖拽和声明式是另外两条 UI 路线，本课程走纯代码是为了让你们看清每一层。会走路了再学开车不迟。"
【收线】"三天了，你们的程序还没碰过磁盘。明天开始，让数据活过程序的重启。"散场。"""
},
]
