diff --git a/docs/teaching/slides/day1.html b/docs/teaching/slides/day1.html index c9156ae..819007e 100644 --- a/docs/teaching/slides/day1.html +++ b/docs/teaching/slides/day1.html @@ -2,7 +2,7 @@
-C++/Qt 方向实训 · 第 1 / 5 天 · 对齐官方方案书 Day1
+C++/Qt 方向实训 · 第 1 / 4 个讲授日 · 对齐 2026-07 新版 10 天安排
本教案假定学员已掌握 C++ 基础语法与 OOP 概念——方案书对今天下午的 - 官方定位是"回顾和提升",不是零基础教学。今天开始的 5 天覆盖方案书 C++方向 - Day1–5 全部课堂内容,Day6–10 是纯项目实作。
+本教案假定学员已掌握 C++ 基础语法与 OOP 概念——今天定位是"回顾和提升", + 不是零基础教学。Day1–4 讲授,Day5 综合案例,Day6–10 项目开发与答辩。 + 大纲:docs/teaching/OUTLINE_4DAY.md
这条假设不是拍脑袋定的——建议模块 B 讲授前先用 - docs/teaching/PRETEST.md - (10 题、10 分钟)实测一次,按结果决定原节奏推进还是回退到语法讲解。
+受众假设要实测——用 docs/teaching/PRETEST.md + (10 题、10 分钟、A/B 平行卷)测一次,按结果决定回顾环节推进速度(分层预案见该文件)。
模块 A · 讲授 35min
-上午 · 讲授 08:30–11:00(讲 25–30min + 随堂练 10–15min 循环)
+
- 出处:docs/TRAINING_PLAN_2026.md 「六、实习实训服务流程」
-
- 出处:docs/TRAINING_PLAN_2026.md 「七、质量保证」
-模块 A · 讨论 95min(分组,非编码)
-今天上午没有编码练习——先把协作流程和项目方向定下来,Day6–10 - 的项目实作会真的用到今天讨论的结论。
-模块 B · 讲授 35min(巡礼式,压缩讲解)
-只挑「学过但容易讲不透」的点,不逐章展开——方案书官方定位是 - "回顾和提升",深度让位于 Day2–5 的 Qt 内容。
-using namespace std;:会把整个命名空间污染带给所有
- include 这个头文件的地方,容易引发命名冲突,规范做法是只在 .cpp 里用,或干脆不用、
- 全程写限定名。int/double 都能转
- float 参数,传一个 long 进去可能触发意料之外的重载版本。
-Base* p = new Derived();
-delete p; // 如果 ~Base() 不是 virtual,~Derived() 不会被调用——经典泄漏
-
- delete 派生类对象时,只有基类析构函数是 virtual
- 才会走虚函数表找到派生类的析构函数——这是"多态"在析构这个场景下的具体体现。virtual——即使当前基类本身没什么要清理的,这是个防御性约定。vector 扩容),
- 继续用失效的迭代器是未定义行为——遍历中增删元素前要先弄清对应容器的迭代器失效规则。出处:p01/ch05/class_template_basics.cpp、 - p01/ch09/s03/vector_iterator.cpp
-auto 类型推导、智能指针
- unique_ptr/shared_ptr(RAII 终结手动 new/delete)、
- 右值引用 && + 移动语义(消除不必要的拷贝)、lambda 表达式、
- 语言级并发 std::thread/mutex/atomic、
- nullptr、constexpr。auto)、函数返回类型自动推导、
- std::make_unique 补齐。auto [a,b] = pair、if/switch
- 带初始化语句、std::optional/variant/string_view、
- 标准库终于有跨平台的 std::filesystem。view | filter | transform 管道式组合)、
- coroutines 协程(co_await 把异步代码写成同步的样子)、modules 模块(挑战头文件
- 体系)、<=> 三路比较一行换 6 个运算符。-std= 支持到哪。本仓库出于教学稳定性选 C++17 作为基线(顶层
- CMakeLists.txt),可用 tools/std_probe.py 自己验证某特性最早从哪个
- 标准开始能编译(宿主 gcc 13.3 原生支持到 C++23)。本节为纯概念巡礼,无对应仓库示例——具体新特性的动手练习超出本次"C++回顾"压缩讲解范畴,
- 感兴趣可用 tools/std_probe.py + tools/demo_versions/ 自行探索
- (参见 docs/VERSION_NOTES.md)。
cmake -S -B 读
- CMakeLists.txt 生成 Ninja/Makefile 工程,cmake --build 才是
- 真正编译。option(BUILD_QT_PART) 控制是否编译 Qt 部分,每个示例一个
- add_executable 目标,这就是"一文件一目标"约定的由来。CMakeLists.txt 生成 Ninja/Makefile 工程(Makefile 是它的生成目标之一——
+ 这就是两者的关系),cmake --build 才是真正编译。
+ option(BUILD_QT_PART) 控制是否编译 Qt 部分,每个示例一个
+ add_executable 目标。
CMakeLists.txt 却没重新 cmake 配置(只 build):
- 大多数改动 CMake 会自动侦测重新配置,但换了生成器/清了缓存等情况需要重新走
- -S -B 一次。CMakeLists.txt 却没重新配置:大多数改动 CMake 会自动侦测,
+ 但换生成器/清缓存后需要重新走一次 -S -B。出处:仓库根 CMakeLists.txt
+出处:仓库根 CMakeLists.txt
+ · 包管理器一分钟:vcpkg 用 toolchain 文件让 find_package 自动找到三方库;
+ 本仓库选"隔离 Qt 手动装",两种思路都要认识。
main.cpp:12:5: error: 'foo' was not declared ← 编译期:带文件行号
+main.o: undefined reference to `Widget::bar()' ← 链接期:没有行号
vcpkg install,配合 CMake 的 toolchain 文件,find_package
- 就能自动找到头文件和库。error: 带行号 = 编译期问题(语法/类型/没声明)——回代码里查那一行;
+ undefined reference 无行号 = 链接期问题(有声明没定义/缺库/缺目标文件)——
+ 查"定义在哪、链没链进来",回头检查语法是南辕北辙。tools/install_qt_host.sh)而非 vcpkg,两种思路都要认识:包管理器省心但
- 引入额外工具链,手动隔离装则精确控制版本/路径,各有取舍。undefined reference 的第一反应是检查拼写和语法——方向就错了。
+ 明天 Qt 的"忘写 Q_OBJECT"报的正是链接错误,且报错完全不提宏,记住今天这页。本仓库暂无 vcpkg 可运行示例——安装真实 vcpkg 工具链超出最小示例范畴,此处仅作概念介绍。
+ + +using namespace std; 会把命名空间污染带给所有 include 它的
+ 地方——只在 .cpp 用,或全程写限定名。出处:p01/ch04/(类与对象各节)、 + p01/ch03/namespace_syntax.cpp, + 对照 docs/ERRATA.md
模块 B · 练习 95min
+上午 · 课堂练习 11:00–12:00(工程师指导、答疑)
下午 · 讲授 13:30–16:00
+long 进去可能触发意料之外的重载版本。
+Base* p = new Derived();
+delete p; // 如果 ~Base() 不是 virtual,~Derived() 不会被调用——经典泄漏
+
+ delete 派生类对象时,只有基类析构是 virtual
+ 才会走虚函数表找到派生类析构——"多态"在析构场景下的具体体现。virtual——防御性约定。
+ (明天会看到 Qt 的 QObject 体系全线虚析构。)unique_ptr 独占所有权、shared_ptr
+ 共享所有权(详见 MODERN_CPP.md §2)。
+ 一个对象只能有一个所有者。vector/map/string
+ 是项目实作三件套;迭代器是"容器和算法之间的统一接口"——STL 算法能脱离具体容器复用的关键。vector 扩容搬家),继续用是
+ 未定义行为——遍历中增删元素前先弄清该容器的失效规则。出处:p01/ch05/class_template_basics.cpp、 + p01/ch09/s03/vector_iterator.cpp
+make_unique;tools/std_probe.py
+ 自己验证某特性从哪个标准开始能编译(宿主 gcc 13.3 支持到 C++23)。自学入口:MODERN_CPP.md(11/14/17, + 示例全部可在本仓库基线下编译) · + MODERN_CPP_20PLUS.md(20/23/26 前瞻, + 含本机 GCC 实测矩阵)
+下午 · 课堂作业 16:00–17:00(工程师答疑)
+tools/std_probe.py 验证一个标准版本差异点(如三目运算符可否作左值)tools/std_probe.py 验证一个标准版本差异点cmake --build build --target all # Part1,144 个目标全绿
- 每组提交一句话分工说明 + 至少 1 条 ERRATA 坑点讲解。
+每人完成构建全绿 + 2 条 ERRATA 坑点讲解;PRETEST 结果已归档(决定后续调速)。
C++/Qt 方向实训 · 第 2 / 5 天 · 讲:练 ≈ 1:2.7
+C++/Qt 方向实训 · 第 2 / 4 个讲授日
模块 A · 讲授 35min
-上午 · 讲授 08:30–11:00
+QApplication 是整个进程唯一的"事件循环拥有者"——app.exec()
- 阻塞在这里进入事件循环,不断从操作系统取事件(鼠标/键盘/定时器/重绘请求)分发出去,
- 程序的"活着"就是这个循环在转,不是运行完就退出的普通程序。Q_OBJECT 宏触发 moc(meta-object compiler)在编译前生成额外代码——
- 信号槽、属性系统、qobject_cast 等运行时反射能力都靠这份生成代码实现,
- 这是 Qt 自己的预处理步骤,不是普通 C++ 语法。Core(对象/容器/IO)、Gui、Widgets(桌面控件)。.qt514/),不碰系统
+ Qt——顶层 CMakeLists 有版本断言,连错版本直接 FATAL_ERROR。这是工程上
+ "锁依赖版本"的真实示范。出处:仓库根 CMakeLists.txt(版本断言一节)
+find_package(Qt5 5.14 REQUIRED COMPONENTS Core Gui Widgets)
+add_executable(minimal_qt_app minimal_qt_app.cpp)
+set_target_properties(minimal_qt_app PROPERTIES AUTOMOC ON)
+target_link_libraries(minimal_qt_app PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)
+ find_package 概念今天落地:CMake 按 CMAKE_PREFIX_PATH
+ 找到 Qt;AUTOMOC 自动跑 moc(元对象编译器)——它为 Q_OBJECT
+ 类生成信号槽所需的代码,下一页就见到。QApplication 是唯一的事件循环拥有者,app.exec() 阻塞进入
+ 事件循环分发系统事件;Q_OBJECT 宏触发 moc 生成信号槽/属性系统所需的元对象代码。QApplication 实例,创建第二个直接崩溃/未定义行为。exec():窗口一闪而过就退出,因为根本没进入事件循环。Q_OBJECT:编译能过,链接阶段报
- undefined reference——报错信息完全不提示"缺宏",是新手最常踩的坑。QApplication;忘记 exec() 窗口一闪而过;Q_OBJECT:报 undefined reference——
+ 链接错误(昨天那页!),报错完全不提"缺宏",知道这一条能省一小时。
-class Widget : public QWidget {
- Q_OBJECT // 用到信号槽/属性系统的类都要有这个宏(触发 moc 代码生成)
-public:
- Widget(QWidget* parent = nullptr) : QWidget(parent) {}
-};
-
-int main(int argc, char* argv[]) {
- QApplication app(argc, argv); // 整个程序唯一的 QApplication
- Widget w;
- w.show();
- return app.exec(); // 进入事件循环,程序在这里“活着”
-}
-
- 出处:p03/ch02/minimal_qt_app.cpp(注意仓库版本额外注入了
- QTimer::singleShot 200ms 自动退出,便于离屏自动化验证,真实项目不需要这一行)
parent 后,
- parent 被销毁时会自动 delete 所有子对象——这就是为什么 Qt 代码里到处是
- new 却几乎不见 delete。parent:控件永远不会自动释放(真实内存泄漏),也不会自动出现在
- 窗口里(没有父容器就没有布局位置)。move() 手动摆放坐标的控件,一旦外层套上布局管理器,手动坐标会被
- 布局在下次刷新时覆盖——这是"混用绝对定位和布局"最常见的诡异 bug 来源。move() 手动定位的控件一旦套上布局会被布局接管覆盖。
-// 第一种:先构造再设属性
-QPushButton* btn = new QPushButton;
-btn->setParent(this);
-btn->setText(QStringLiteral("德玛西亚"));
-btn->move(100, 100);
-
-// 第二种:构造时指定文本与父亲
-QPushButton* btn2 = new QPushButton(QStringLiteral("孙悟空"), this);
+QPushButton btn("我建在栈上"); // 先声明 → 后析构
+QWidget window; // 后声明 → 先析构
+btn.setParent(&window);
+// 离开作用域:window 先析构 → 父子树 delete &btn(栈地址!)→ 崩溃
-
- connect() 背后依赖 moc 生成的元对象信息做类型检查:新语法
- &Class::signal 是编译期检查,字符串旧语法
- SIGNAL()/SLOT() 是运行时检查,容易悄悄连接失败。SIGNAL(clicked()) 函数名/参数类型写错不会编译报错,
- 只会运行时"连接失败"且经常没有提示——能用新语法就不用旧语法,编译期就能发现问题。hungry())不加 QOverload
- 消歧义,编译器不知道该连哪个重载,直接报错。tools/run_qt.sh qt_pitfall_stack_widget --crash(真崩给你看)。
-class Teacher : public QObject {
- Q_OBJECT
-signals:
- void hungry(); // 无参信号
- void hungry(QString food); // 带参信号(重载)
-public slots:
- void classIsOver(){ emit hungry(); emit hungry(QString("汉堡")); }
-};
-// 连接重载信号时必须用 QOverload 显式指定版本
-connect(t, QOverload<>::of(&Teacher::hungry), s, QOverload<>::of(&Student::treat));
-connect(t, QOverload<QString>::of(&Teacher::hungry), s, QOverload<QString>::of(&Student::treat));
-
- 出处:p03/ch04/custom_signal_slot.cpp; - lambda 槽 + 值捕获/引用捕获对比见 - lambda_signal_slot.cpp
+复现示例:examples/pitfall_stack_widget/ + (README)
模块 A · 练习 95min
-上午 · 课堂练习 11:00–12:00(工程师指导、答疑)
+minimal_qt_app 与 button_creationminimal_qt_app 与 button_creation;把窗口标题和
+ 按钮文字改成自己的学号+姓名(中文——如果乱码了,记住现象,下午有一页专讲)QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app 与
- button_creation 均退出码为 0,无崩溃QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app 退出码 0;
+ 有 X 环境下能看到自己改的中文标题正常显示QLabel 的文本内容QLabel 文本确实改变;
- 用 connect 语法而非直接函数调用下午 · 讲授 13:30–16:00
+emit,不知道谁在听——对象间彻底解耦;&Class::signal 编译期类型检查;旧字符串语法
+ SIGNAL()/SLOT() 只在运行时检查;连接方式 Direct/Queued/Auto(Day5 跨线程时回收)。QOverload 消歧义直接编译报错。出处:p03/ch04/builtin_signal_slot.cpp · + custom_signal_slot.cpp · + lambda_signal_slot.cpp
+
+connect(sender, &Sender::sig, [obj]{ obj->f(); }); // ✗ obj 销毁后仍触发
+connect(sender, &Sender::sig, obj, [obj]{ obj->f(); }); // ✓ 第五参数=context
+
+ 复现示例:examples/pitfall_lambda_capture/
+ (README,
+ --dangle 看真实悬空访问)
connect 每调一次新增一条连接,Qt 不去重——同一对信号槽连两次,
+ 槽就被调两次。修复:connect 收拢到构造函数只做一次(首选);或
+ Qt::UniqueConnection(只对成员函数指针有效,lambda 防不了)。复现示例:examples/pitfall_double_connect/ + (README)
+QString 内部是 UTF-16,和 std::string(字节串)是两种东西,
+ 互转必须指明编码:toUtf8()/fromUtf8();中文字面量建议
+ QStringLiteral("中文") + 源码文件保存为 UTF-8。QString(char*) 隐式转换中文字面量 → 界面乱码。
+ 上午改学号姓名乱码的同学:根源就在这页。Day4 读写文件时编码问题还会再来一次。builtin_signal_slot.cpp,给一个类定义两个同名不同参的信号,用
- QOverload 消歧义连接
+auto* grid = new QGridLayout(window); // 布局原理明天下午讲透
+grid->addWidget(btn7, 0, 0); grid->addWidget(btn8, 0, 1); // 今天先会用:
+grid->addWidget(btn9, 0, 2); grid->addWidget(btnDiv, 0, 3); // 行、列坐标摆键盘
+
+ button_creation)+ 点击信号槽(刚学的三种写法)
+ + QGridLayout 脚手架排键盘 + QLabel/QLineEdit 显示结果。模块 B · 讲授 35min
-:/Image/boat.jpg 这种路径不是文件系统路径,是 Qt 资源系统(QRC)——
- 图片等资源在编译期被打包进可执行文件本身,运行时不依赖外部文件是否存在,这对
- "打包发布"很关键。.qrc 里资源路径/文件名和代码里 :/... 引用的不一致,会在
- 运行时静默失败(QPixmap 为空、图不显示),不会报编译错误。.qrc 接入 CMake 的 qt5_add_resources/AUTORCC,
- 资源系统压根不生效。
-QPixmap pixmap;
-pixmap.load(":/Image/boat.jpg"); // 从 Qt 资源系统(.qrc)加载
-QLabel* label = new QLabel(this);
-label->setPixmap(pixmap);
-
- 出处:p03/ch08/label_pixmap.cpp; - HTML 富文本标签见 label_text_html.cpp
-QLayout)接管子控件的位置和大小计算,随窗口缩放自动重新
- 排布——这是"不要自己算坐标"的根本原因,也是跨平台/跨分辨率 UI 能正确显示的基础。new QHBoxLayout(this)
- 把布局设为 this 的布局,同时隐式接管了 addWidget 进来的子控件。addWidget 到不同布局会导致界面
- 异常或崩溃。setGeometry/move 手动改控件位置,效果
- 会被布局在下次刷新时覆盖(呼应上一节"控件创建"的坑)。
-QSpinBox* spin = new QSpinBox(this);
-QSlider* slider = new QSlider(Qt::Horizontal, this);
-QHBoxLayout* layout = new QHBoxLayout(this);
-layout->addWidget(spin);
-layout->addWidget(slider);
-// spin 变化 → slider 跟随,slider 变化 → spin 跟随
-connect(spin, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), slider, &QSlider::setValue);
-connect(slider, &QSlider::valueChanged, spin, &QSpinBox::setValue);
-
- 出处:p03/ch08/custom_widget.cpp——布局管理器负责排布, - 不用手算控件坐标。
-QPushButton、ID 选择器
- #okButton、伪状态 :hover/:pressed)+ 属性声明,
- 但作用对象是 QWidget 树而不是 DOM。#okButton)必须先 setObjectName("okButton"),
- 忘记设置会导致规则完全不生效且没有任何报错。setStyleSheet 会级联影响它所有子控件(包括不想改的),
- 范围失控是 QSS 最容易踩的坑,通常要用更具体的选择器限定范围。
-okBtn->setObjectName("okButton");
-setStyleSheet(
- "QPushButton#okButton { background-color: #2f855a; border-radius: 6px; color: white; }"
- "QPushButton#okButton:hover { background-color: #276749; }"
-);
-
- 出处:docs/teaching/examples/qss_styling/qt_qss_styling.cpp
- (配套 README)——
- 仓库 p03/ 的 wiki 抓取内容没有 QSS,这是本轮补写的教学补充示例,已离屏验证跑通。
模块 B · 练习 95min(计算器案例,练习任务,非预建方案)
-下午 · 课堂作业 16:00–17:00(工程师答疑)
+button_creation.cpp(数字/运算符按钮)+ custom_signal_slot.cpp
- (点击信号槽)+ custom_widget.cpp(布局排布按钮),做出一个能加减乘除的最小计算器QLabel/QLineEdit 上正确显示结果;
- 界面不重叠、按钮可点击background-color;至少 1 个按钮有
- :hover 或 :pressed 效果QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>
-# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口
- 现场演示计算器,至少完成一次正确运算 + QSS 美化。
+现场演示计算器完成至少一次正确运算(QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>
+ 或有 X 环境直接运行);中文界面文字无乱码。
C++/Qt 方向实训 · 第 3 / 5 天 · 讲:练 ≈ 1:2.7
+C++/Qt 方向实训 · 第 3 / 4 个讲授日
今天的主线:把昨天的计算器升级成"正经软件"——主窗口骨架 + 对话框交互 + + 布局讲透 + 美化。
模块 A · 讲授 35min
-上午 · 讲授 08:30–11:00
+QAbstractItemModel
- 及其子类)只负责存数据、回答"这个位置是什么",视图(QListView/
- QTableView/...)只负责画——同一份模型可以同时喂给多个不同视图。QModelIndex(行/列/父项)寻址数据,视图向模型要数据时
- 还要带上"角色"(Qt::DisplayRole 显示文本、Qt::DecorationRole
- 图标……),同一个位置可以按不同角色返回不同内容。setCentralWidget()
+ 显式安放;菜单项和工具栏按钮的统一抽象是 QAction(一个动作,
+ 多处复用)。dataChanged/beginInsertRows+
- endInsertRows),视图不会自动刷新,界面和数据"对不上"。QStandardItemModel 却又想自定义复杂数据结构:应该继承
- QAbstractItemModel 自己实现,而不是硬塞进 Item 模型。QAction 显示出来 ≠ 生效:忘了 connect 到槽,
+ 就是"菜单点了没反应"。exec():阻塞等用户给结果,返回值就是用户的选择
+ (Accepted/Rejected);非模态 show():立即返回,窗口独立存活
+ ——所以对话框对象必须活过当前函数作用域(堆 + parent,或
+ WA_DeleteOnClose 自清理)。QMessageBox(提示/确认)、QFileDialog(选文件)。show() 完函数一返回就析构——窗口一闪而过。
+ p03/ch06 的三个 modeless 版本(栈/堆/DeleteOnClose)就是这个坑的对照教材。出处:p03/ch06/modal_dialog.cpp · + modeless_dialog_stack.cpp · + modeless_dialog_heap.cpp · + modeless_dialog_deleteonclose.cpp
+
-// 模型:只管数据
-auto* model = new QStandardItemModel(this);
-model->appendRow(new QStandardItem(QStringLiteral("需求分析")));
+auto ret = QMessageBox::question(this, "确认", "真的要删除吗?");
+if (ret == QMessageBox::Yes) { /* ... */ }
-// 视图:只管展示,不持有数据
-auto* view = new QListView(this);
-view->setModel(model);
-
-// 选中状态变化 → 通知外部,模型和视图之间没有直接耦合
-connect(view->selectionModel(), &QItemSelectionModel::currentChanged, ...);
+QString path = QFileDialog::getOpenFileName(this, "选择文件", "", "*.json");
+if (!path.isEmpty()) { /* ... */ } // 用户可能点取消:必须处理空返回
- 出处:docs/teaching/examples/model_view/qt_model_view_basics.cpp
- (配套 README)——
- 仓库 p03/ 的 wiki 抓取内容没有 Model/View,这是本轮补写的教学补充示例,已离屏验证跑通。
app.exec() 进入的就是事件循环(复用
- Day2 概念)——鼠标点击、
- 键盘输入、定时器触发都会变成"事件"(QEvent),被塞进这个循环里依次分发给
- 对应控件。event()/xxxEvent() 虚函数或事件过滤器;信号槽是"对象之间"
- 主动通知,走 connect()。控件收到事件后,常见做法就是在事件处理函数里
- emit 一个信号,把"底层事件"转成"业务语义通知"。app.exec() 进入的就是事件循环——鼠标点击、键盘输入、定时器
- 触发都会变成事件,被塞进这个循环里依次分发。今天讲的"事件过滤器"就是在
- 事件被分发给控件之前,插一个"拦截点"。
xxxEvent()(只影响这一个控件自己)或装事件过滤器
- installEventFilter()(可以在事件到达目标控件之前,由另一个对象统一拦截,
- 常用于"一个地方管多个控件"的场景)。accept()/ignore() 状态:处理完事件要表明"我
- 吃掉了这个事件"还是"继续往上传",决定事件是否会继续传播给父控件。xxxEvent() 时忘记调用基类实现(如 QWidget::mouseMoveEvent),
- 可能丢失控件本身需要的默认行为。
-class EventLabel : public QLabel {
- Q_OBJECT
-public:
- explicit EventLabel(QWidget* p=nullptr):QLabel(p){ setMouseTracking(true); }
-protected:
- void mouseMoveEvent(QMouseEvent* event) override {
- setText(QString("Move: (%1, %2)").arg(event->x()).arg(event->y()));
- }
-};
-
- 重写 xxxEvent 是 Qt 事件处理的主线之一(另一条线是事件过滤器
- installEventFilter)。出处:p03/ch09/mouse_event.cpp;
- 事件过滤器见 event_filter_mainwindow.cpp
出处:p03/ch06/message_box_question.cpp · + message_box_custom.cpp · + file_dialog.cpp
模块 A · 练习 95min
-上午 · 课堂练习 11:00–12:00(工程师指导、答疑)
+qt_model_view_basics.cpp 里的 QListView 换成
- QTableView,模型代码不改QMessageBox;加一个「历史」停靠窗(可先放空 QLabel)event_filter_mainwindow.cpp,给一个控件装事件过滤器,拦截鼠标按下事件并打印坐标模块 B · 讲授 35min
-下午 · 讲授 13:30–16:00
+QFile/QTextStream/QDataStream 都建立在
- QIODevice 这一统一读写接口上——同样的"打开/读/写/关闭"套路,以后遇到网络
- 套接字/串口一类设备也是同一套接口,学一次可以迁移到别的设备类型。QTextStream 面向"文本行"(人可读),QDataStream 面向
- "结构化二进制"(保留类型信息,程序间高效互传但人不可直接读)——按需求选,不是谁更好。QGridLayout 今天讲透:QHBox/QVBox/QGrid 三兄弟 + 嵌套组合 + 伸缩因子
+ addStretch();布局对象同样遵循 QObject 父子所有权。open() 的返回值:文件不存在/无权限时静默失败,后续读写在一个
- 没打开的设备上操作,容易读出空数据而不报错。QIODevice::Text):文本模式下 Qt 会做换行符
- 转换,用来读写二进制数据会破坏内容。setGeometry()/move() 手动改位置——
+ 会被下次布局刷新覆盖,看起来"设置没生效"。
-QFile file("in.txt");
-if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "打开失败"; return -1; }
-while (!file.atEnd()) { qDebug() << file.readLine(); }
-
- 出处:p03/ch11/qfile_basic.cpp;结构化读写见 - qtextstream_io.cpp、 - qdatastream_io.cpp
-QJsonDocument::toJson)
- 把内存数据变成可存/可传的文本,反序列化(fromJson)再变回来——正确性的标准是
- "写出去再读回来,数据要一致"(round-trip)。QJsonValue 是一个变体(variant)类型,能装字符串/数字/布尔/数组/对象,
- 取值时要按预期类型调用 toString()/toInt() 等。QPixmap)/
+ 动图(QMovie);输入靠 QLineEdit,按钮族 QPushButton/QRadioButton/QCheckBox。:/... 路径是 Qt 资源系统:资源编译期打包进可执行文件,运行时不依赖
+ 外部文件——发布不用带一堆散图。QJsonDocument::fromJson 的 QJsonParseError:
- 文本格式错误时返回一个空文档,后续取值全是默认值,不会崩溃也不会报错,很难定位。toInt()/toString() 等不会抛异常,只会静默
- 返回默认值(0/空串),排查时容易误以为"数据没写进去"。.qrc 里的路径与代码引用不一致:运行时静默失败(图不显示),
+ 不报编译错误;忘接 CMake 的 AUTORCC/qt5_add_resources 资源系统不生效。出处:p03/ch08/label_text_html.cpp · + label_pixmap.cpp · + label_movie.cpp
-QJsonObject student;
-student["name"] = QStringLiteral("张三");
-student["score"] = 92;
-
-QJsonDocument doc(student);
-QByteArray bytes = doc.toJson(QJsonDocument::Indented); // 序列化
-
-QJsonDocument parsed = QJsonDocument::fromJson(bytes); // 反序列化
-QJsonObject obj = parsed.object();
-qDebug() << obj["name"].toString() << obj["score"].toInt();
-
+ QPushButton#okButton { background-color: #2f855a; border-radius: 6px; }
+QPushButton#okButton:hover { background-color: #276749; }
+ #objectName)/ 伪状态
+ (:hover :pressed)+ 层叠继承——作用对象是 QWidget 树而非 DOM;
+ "长什么样"与"做什么"分离,改样式不碰逻辑。setObjectName();setStyleSheet() 会级联影响所有子控件——范围失控是最容易踩的坑,
+ 样式尽量写窄(带选择器),不要裸写属性。出处:docs/teaching/examples/json_io/qt_json_parse_write.cpp
- (配套 README)——
- 仓库 p03/ch11 的 wiki 抓取内容只有 QFile/QTextStream/QDataStream,没有 JSON,
- 这是本轮补写的教学补充示例;不需要 GUI,tools/run_qt.sh qt_json_parse_write
- 直接跑即可,不需要 QT_QPA_PLATFORM=offscreen。
模块 B · 练习 95min(本地数据存储案例,练习任务,非预建方案)
-下午 · 课堂作业 16:00–17:00(工程师答疑)
+qt_json_parse_write.cpp 的 JSON 数据实际写入一个文件(结合
- qfile_basic.cpp 的 QFile 写法),再读回来解析校验.json 文件,内容可读;重新运行程序能正确解析出
- 之前写入的数据background-color;至少 1 个按钮有
+ :hover 或 :pressed 效果;拉伸窗口按钮跟着重排现场演示本地数据存储小工具,验证"写入 → 重启 → 读回"完整闭环。
+现场演示升级后的计算器:主窗口(菜单/停靠窗)+ QSS 配色 + 缩放自适应,功能不回退。
C++/Qt 方向实训 · 第 4 / 5 天 · 讲:练 ≈ 1:2.7
+C++/Qt 方向实训 · 第 4 / 4 个讲授日
今晚的作业「本地数据存储小工具」是明天 Day5 综合案例的直接前身。
模块 A · 讲授 35min
-上午 · 讲授 08:30–11:00
+paintEvent
- 里,由 Qt 在需要重绘时(窗口刚显示、被遮挡后重新露出、你主动请求)自动调用——这和直接式
- 绘图(想画就立刻画到屏幕)是不同的模型。update()(异步,排队到下次事件循环处理)而不是
- 直接调用 paintEvent();QPainter 只是"在哪个设备上画"的画笔,
- 画布可以是控件本身,也可以是 QImage/QPixmap 等离屏设备。app.exec() 今天拆开看:事件循环不停从队列取 QEvent
+ 派发给目标控件——事件是框架推给控件的通知(鼠标/键盘/重绘),信号槽是
+ 对象间的主动通知,两套机制并存不冲突。出处:p03/ch02/minimal_qt_app.cpp(exec())
mousePressEvent() 等虚函数(只影响自己);
+ installEventFilter()(一个对象统一拦截多个控件的事件)。
+ 事件有 accept()/ignore() 状态,决定是否继续向父控件传播。paintEvent 之外的地方直接 new QPainter(this) 画东西:
- 要么无效要么未定义行为,绘制必须在 paintEvent 回调期间进行。update():界面不会自动感知"要重画了",看起来像是
- "画错了",其实是"根本没重画"。QWidget::mousePressEvent(e))——
+ 控件默认行为悄悄丢失,现象诡异难查。出处:p03/ch09/event_override_customwidget.cpp · + event_filter_mainwindow.cpp · + mouse_event.cpp
-void paintEvent(QPaintEvent*) override {
- QPainter painter(this);
- painter.drawLine(QLine(10, 10, 200, 10));
- painter.drawRect(QRect(10, 30, 100, 50));
- painter.drawEllipse(QRect(150, 30, 100, 80));
-}
-
- 所有绘制都发生在 paintEvent 里,由 Qt 在需要重绘时自动调用。
- 出处:p03/ch10/qpainter_basic.cpp;绘图设备
- (QImage/QPixmap/QPicture)见
- paint_device_qimage.cpp、
- paint_device_pixmaps.cpp
moveToThread 移动到的那个线程,它的槽函数默认在所属线程执行——GUI 控件必须
- 始终归属主线程,这是 Qt 线程模型的核心约束。moveToThread 到工作线程),而不是继承 QThread 重写
- run()——前者能继续用信号槽通信,后者容易把线程亲和性搞混。paintEvent()
+ 里,由 Qt 按需调用;数据变了想重画,调 update()(异步排队一次重绘请求)。
+ 绘图设备三件套 QImage/QPixmap/QPicture 作参考。label->setText()):Qt 的 GUI
- 模块不是线程安全的,这是未定义行为/崩溃的常见来源——正确做法永远是发信号,排队回主线程处理。moveToThread 要在 connect 和 thread.start()
- 之前完成;对象一旦有耗时任务在跑,中途再挪线程会有竞态。paintEvent 之外直接构造 QPainter 画窗口——未定义行为;update()——界面不会自动重画。出处:p03/ch10/qpainter_basic.cpp + (设备类参考:paint_device_qimage.cpp 等)
-class Worker : public QObject {
- Q_OBJECT
-public slots:
- void process() {
- qint64 sum = 0;
- for (int i = 1; i <= 1000000; ++i) sum += i;
- emit resultReady(sum); // 排队连接自动把结果带回主线程
- }
-signals:
- void resultReady(qint64 result);
-};
-worker.moveToThread(&workerThread); // 官方推荐写法:worker-object + moveToThread
-connect(&workerThread, &QThread::started, &worker, &Worker::process);
-
+ QTableWidgetItem,new 出来交给表格
+ 接管所有权),上手快、中小项目够用;"表格事件"以信号暴露:
+ cellClicked / cellDoubleClicked / itemChanged。dataChanged 等信号视图才会刷新。数据规模大/多视图联动时的
+ 正解,自学入口:examples/model_view/。出处:docs/teaching/examples/qthread_worker/qt_thread_worker.cpp
- (配套 README)——
- 仓库 p03/ 的 wiki 抓取内容没有多线程,这是本轮补写的教学补充示例,已验证跑通。
模块 A · 练习 95min
-上午 · 课堂练习 11:00–12:00(工程师指导、答疑)
+qpainter_basic.cpp,在 paintEvent 里画一个组合图形
- (如棋盘格/仪表盘刻度)paintEvent 里画一个组合图形(棋盘格或仪表盘刻度)qt_thread_worker.cpp 里的耗时计算换成一个更长的循环或
- QThread::msleep,观察主线程/事件循环没有被卡住moveToThread 起的作用模块 B · 讲授 35min
-下午 · 讲授 13:30–16:00
+Q_PROPERTY,同样靠
- Day2 讲过的 moc 生成)给每个属性配了
- 统一的 get/set 接口;动画框架的本质就是"在一段时间内,用缓动曲线不断调用某个属性的
- setter",所以任何声明成 Q_PROPERTY 的属性(不只是内置的 geometry/opacity)
- 都能被动画驱动。QEasingCurve 决定的是"数值随时间变化的曲线形状"(线性/回弹/加减速……),
- 动画本身只是"定时器 + 插值 + 调 setter"的组合,理解这点能解释为什么几乎所有属性都能做动画。QFile/QTextStream/QDataStream 都建立在
+ QIODevice 统一接口上——之后遇到网络/串口也是同一套;
+ QTextStream 面向文本行,QDataStream 面向结构化二进制。Q_PROPERTY(有 setter),随手传一个字符串
- 属性名如果对应类没有声明该属性,动画不会报错,只是什么也不会发生。QAbstractAnimation::DeleteWhenStopped 能让动画结束后自动释放自身,但不管
- 目标对象的生命周期,目标对象仍需自己保证不提前销毁。open() 返回值不检查——文件不存在/没权限时静默失败,
+ 后续读写全是空操作;出处:p03/ch11/qfile_basic.cpp · + qtextstream_io.cpp · + qdatastream_io.cpp
-auto* anim = new QPropertyAnimation(button, "geometry", &window);
-anim->setDuration(800);
-anim->setStartValue(QRect(10, 10, 100, 30));
-anim->setEndValue(QRect(180, 140, 120, 40));
-anim->setEasingCurve(QEasingCurve::OutBounce);
-anim->start(QAbstractAnimation::DeleteWhenStopped);
-
- 出处:docs/teaching/examples/property_animation/qt_property_animation.cpp - (配套 README)
-bool 标志位加 if-else 判断当前处于什么状态,不如让"状态"本身成为一等对象
- (QState),每个状态自己声明"进入这个状态要怎样"(assignProperty,
- 又用到了 Qt 属性系统)。addTransition)本质是"信号触发的转移"——转移条件就是
- 一个信号,这让状态机和信号槽自然衔接:任何能发信号的事件都能驱动状态切换。QJsonDocument/Object/Array/Value 四件套,
+ QJsonValue 是能装多种类型的变体。machine.setInitialState(...) 或忘记调用 machine.start():
- 状态机对象存在但从未真正运行,所有 assignProperty/addTransition
- 都不会生效。QJsonParseError:格式坏的输入静默变成空文档;toInt()/toString() 不报错只返回默认值——
+ 数据悄悄丢,很难查。
+QFile bad("data.json"); // ✗ 相对当前工作目录——它在哪取决于你怎么启动
+QFile good(QCoreApplication::applicationDirPath() + "/data.json"); // ✓ 锚定可执行文件位置
+
+ applicationDirPath() 拼路径,或只读资源走 .qrc(昨天的资源卡)。
-auto* redState = new QState(&machine);
-auto* greenState = new QState(&machine);
-redState->assignProperty(&button, "text", QStringLiteral("红灯:禁止通行"));
-greenState->assignProperty(&button, "text", QStringLiteral("绿灯:允许通行"));
-redState->addTransition(&button, &QPushButton::clicked, greenState);
-greenState->addTransition(&button, &QPushButton::clicked, redState);
-machine.setInitialState(redState);
-machine.start();
+QTextStream ts(&file);
+ts.setCodec("UTF-8"); // Qt 5 默认编码随平台/区域设置而变——跨机器必须显式指定
- 出处:docs/teaching/examples/state_machine/qt_state_machine.cpp
- (配套 README)——
- QStateMachine/QState 属于 QtCore,不需要额外模块。以上两个
- (动画、状态机)都是仓库 p03/ 原本没有的教学补充示例,已离屏验证跑通。
模块 B · 练习 95min(交互式小游戏案例,练习任务,选做,非预建方案)
-下午 · 课堂作业 16:00–17:00(工程师答疑)
+applicationDirPath() 拼(今天的坑卡现学现用)mouse_event.cpp)+ 多线程计时
- (Day4 qt_thread_worker.cpp),做一个"状态变化后限时点击"的最小交互游戏QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>
-# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口
- 分组演示动画+状态机组合小工具(任务卡 2 选做,量力而行)。
+现场演示「写入 → 重启 → 读回」完整闭环;讲得出 open()/ParseError 两处检查为什么不能省。
C++/Qt 方向实训 · 第 5 / 5 天 · 最后一天课堂内容
+C++/Qt 方向实训 · 综合案例日 · 明天起进入项目开发
今天结束后 Day6–10 是纯项目实作(学员编码 + 教员指导 + 成果展示), - 不再有课堂讲授——今天的分组结论会直接带到项目实作阶段。
+目标:通过一个完整小软件讲清 Qt 开发的流程、规范及方法—— + 项目 = 已学知识点的组装。参考实现: + examples/student_manager/ + (README)。 + Day6–9 学员项目开发,Day10 下午答辩。
模块 A · 讲授 35min
-上午 1 · 需求拆解 → 界面骨架
+
- 出处:docs/TRAINING_PLAN_2026.md 「七、质量保证 (二) Git」
+setCentralWidget(table);exec() + QFormLayout + QDialogButtonBox;现场跑:tools/run_qt.sh qt_student_manager(离屏自测版:
+ QT_QPA_PLATFORM=offscreen,自动走完整验证流程)
上午 2 · 数据层
+
+struct Student { QString name; double score; QString tag; };
+QJsonObject toJson(const Student&); // 互逆的一对
+Student fromJson(const QJsonObject&); // round-trip = 正确性标准
+
+ Student 结构体不知道 QTableWidget 的存在——
+ 这是 Model/View 思想的朴素版;open() 返回值、QJsonParseError、
+ 路径用 applicationDirPath() 拼。下午 1 · 提升:多线程防卡界面(新授)
+msleep 循环直接放按钮槽里 → 整个窗口冻结:点不动、拖不动、
+ 系统提示"未响应"——Day4 讲过:主线程阻塞 = 事件循环停摆。
+ 这是学员项目里最高频的"卡死"来源。
+auto* thread = new QThread(this);
+auto* worker = new ImportWorker; // 不给 parent:有 parent 禁止 moveToThread
+worker->moveToThread(thread); // 必须在 start() 之前
+connect(thread, &QThread::started, worker, [worker]{ worker->doImport(10); });
+connect(worker, &ImportWorker::rowReady, this, /* 更新表格 */); // 自动 Queued
+thread->start();
+
+ moveToThread
+ 搬进工作线程,而不是继承 QThread 重写 run;moveToThread 写在 start() 之后 / worker 带了 parent——都不生效。最小版:examples/qthread_worker/ · + 案例集成版:student_manager 的 startImport()
+下午 2 · 开发流程与规范 + 分组定方向
+
- 出处:docs/TRAINING_PLAN_2026.md 「七、(五) 标准化的考核标准」
-模块 A · 讨论 95min
-模块 B · 讲授 35min
-请 1–2 组现场展示计算器 / 本地数据存储 / 动画状态机小游戏,其他组观摩提问。
-cv::Mat ↔ QImage)配合,是很多
- 桌面视觉类项目的典型组合方式。方案书原文对本项标注"可选择",属于非必修拓展。引入 OpenCV 会新增一条完整的
- 三方库工具链,本仓库暂不提供可运行示例。如果感兴趣,自学方向是
- cv::CascadeClassifier 人脸检测入门。
OpenCV 官方教程(doc.opencv.org,选读·需联网)
-模块 B · 练习 95min(综合演练)
-给界面加动效:QPropertyAnimation(任何 + Q_PROPERTY 都能被缓动曲线驱动) · + 多状态 UI:QStateMachine(状态成为一等对象, + 替代一堆 bool 标志)· 线程的更高抽象:QtConcurrent/线程池。
每组现场演示深化后的案例,教师按考核标准图三大块给出反馈,为 Day6–10 - 的正式项目实作分组定方向。
+QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_student_manager
+# 自测 1/2:JSON 写入→清空→读回,2 条记录完整
+# 自测 2/2:后台线程导入 3 条,总计 5 条,界面未阻塞
+ 每组提交:项目选题 + 分工说明 + 任务拆解草稿。明天 Day6 起:学员编码 + 工程师指导, + Day10 下午项目答辩。
系统梳理了实现这 16 个项目所需的 12 个辅助专业领域——数据结构与算法、 - 2D 游戏与碰撞、计算机图形学、数字图像处理、计算机视觉与机器学习(OpenCV/Haar/LBPH)、 - 编译原理与状态机、编码理论、计算机网络、并发编程、文件系统与增量同步、软件设计模式等, - 每个核心知识点附 2–3 个已验证可用的网络课程链接(中英文兼顾)。
-全文(serve.py 下本地一点即开):docs/teaching/SUPPORTING_KNOWLEDGE.md - → qt-course-projs/docs/supporting-knowledge.md
-对齐官方《四川大学软件学院 2026 年夏 C/C++ 项目实习实训方案书 V1.2》
- 「五、执行计划 (二) C++方向」Day1–5(Day6–10 是纯项目实作,无课堂内容,不在本教案范围)。
- 配套大纲:docs/teaching/OUTLINE.md;
- 方案书全文:docs/TRAINING_PLAN_2026.md。
- 离线可用(reveal.js 已 vendor 进仓库);代码链接需通过本地 HTTP 服务打开才能正常跳转,
- 见仓库 docs/teaching/OUTLINE.md 「材料与播放方式」一节。
对齐 2026-07 新版 10 天实训安排(Day1–4 讲授、Day5 综合案例讲解、
+ Day6–10 学员项目开发与答辩)。配套大纲:
+ docs/teaching/OUTLINE_4DAY.md
+ (含新安排简表、逐天设计与中初级学员适配性评估);
+ 旧 5 天版大纲 OUTLINE.md 保留作历史参照。
+ 离线可用(reveal.js 已 vendor 进仓库);代码链接需通过本地 HTTP 服务打开才能正常跳转
+ (python3 -m http.server 8000,见大纲「材料与播放方式」)。