教案对齐官方方案书:重写为 5 天 C++/Qt 教学设计,补齐 6 个 Qt 技术缺口示例

- docs/teaching/OUTLINE.md、slides/day1-5.html、index.html:替换此前自定义的
  4 天方案,严格对齐 docs/TRAINING_PLAN_2026.md「C++方向」Day1-5 课堂内容
  (Day6-10 为纯项目实作,不在教案范围)
- docs/teaching/examples/:官方方案书要求但仓库 p03/(wiki 抓取内容) 未覆盖的
  QSS/Model-View/JSON/QThread/QPropertyAnimation/QStateMachine 六个技术点,
  各补一个最小可运行示例 + README 教学文档,均已离屏验证跑通;手工维护,
  不属于 gen_part3.py 生成产物
- 根 CMakeLists.txt:接入 docs/teaching/examples 构建(BUILD_QT_PART 分支内),
  不改动生成器管理的 p01/p03 CMakeLists
- 任务卡改为目标/验收标准/基础知识参考三段式模板,custom.css 配套新增样式
- docs/teaching/PRETEST.md:10 题 10 分钟 C++ 摸底测验,验证教案假定的受众
  基础是否成立,并与 OUTLINE.md §0 的分层预案挂钩
This commit is contained in:
张宗平
2026-07-02 11:31:44 +08:00
parent 6bb38563b5
commit 83620c0cf5
23 changed files with 1862 additions and 1041 deletions
+28
View File
@@ -0,0 +1,28 @@
# 手工维护 —— 不是 tools/gen_part3.py 的生成产物,重新生成 p01/p03 不会影响这里。
#
# 教学补充示例:方案书「C++方向」课程要求、但仓库 p03/(wiki 抓取内容)未覆盖
# 的技术点,每个子目录一个最小可运行 Qt 程序 + 配套 README.md 教学文档。
# 不计入 README.md/CLAUDE.md 里「144+29=173 个目标」的生成器目标计数。
function(add_teaching_example name dir)
add_executable(${name} ${dir}/${name}.cpp)
set_target_properties(${name} PROPERTIES
CXX_STANDARD 17 CXX_STANDARD_REQUIRED ON CXX_EXTENSIONS OFF AUTOMOC ON)
target_link_libraries(${name} PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)
target_compile_options(${name} PRIVATE -Wall -Wextra)
endfunction()
add_teaching_example(qt_qss_styling qss_styling)
add_teaching_example(qt_model_view_basics model_view)
add_teaching_example(qt_json_parse_write json_io)
add_teaching_example(qt_thread_worker qthread_worker)
add_teaching_example(qt_property_animation property_animation)
add_teaching_example(qt_state_machine state_machine)
add_custom_target(all_teaching_examples DEPENDS
qt_qss_styling
qt_model_view_basics
qt_json_parse_write
qt_thread_worker
qt_property_animation
qt_state_machine)
+56
View File
@@ -0,0 +1,56 @@
# JSON 解析与写入 —— 教学补充示例
> 对应方案书「C++方向 Day3」:"Qt 文件 IO 操作与 JSON 等主流数据格式解析"。
> 仓库 `p03/ch11` 的 wiki 抓取内容覆盖了 `QFile`/`QTextStream`/`QDataStream`
> 唯独没有 JSON,本示例是手工补写的教学补充,**不属于**
> `tools/gen_part3.py` 的生成产物。
## 概念
Qt 从 5.0 起自带 JSON 支持(`QtCore` 模块,不需要额外依赖第三方 JSON 库),
核心是四个类:
- `QJsonValue` —— 一个 JSON 值(字符串/数字/布尔/数组/对象/null);
- `QJsonObject` —— 键值对集合,对应 JSON 里的 `{ ... }`
- `QJsonArray` —— 值的列表,对应 JSON 里的 `[ ... ]`
- `QJsonDocument` —— 顶层容器,负责整个文档和字节流(`QByteArray`)之间的
相互转换(序列化/反序列化)。
`p03/ch11` 已有的 `QTextStream`/`QDataStream` 对比:那两个解决的是"文本行"
和"二进制结构体"的读写;JSON 解决的是"带层级结构的数据"(对象嵌数组嵌对象)的
读写,是目前最常见的配置文件/网络接口数据格式。
## 代码走读
见 [`qt_json_parse_write.cpp`](qt_json_parse_write.cpp)
1.`QJsonObject`/`QJsonArray` 构造一份"学员信息"数据;
2. `QJsonDocument(obj).toJson(QJsonDocument::Indented)` 序列化成带缩进的可读
字节流(`QJsonDocument::Compact` 则是不带空白的紧凑格式,写文件/网络传输
常用这个);
3. `QJsonDocument::fromJson(bytes)` 反序列化回来,`.isObject()` 判断顶层类型后
逐个字段取值。
这个示例不涉及 GUI,纯粹是数据结构的构造/序列化/反序列化,所以用
`QCoreApplication` 而不是 `QApplication`,不需要 `QT_QPA_PLATFORM=offscreen`
## 验证方式
```bash
tools/run_qt.sh qt_json_parse_write
```
预期终端输出:先打印一段带缩进的 JSON 文本,再打印从中解析出的姓名/分数/标签。
## 练习方向(供任务卡引用)
- 把结果写入一个真实文件(结合 `p03/ch11/qfile_basic.cpp``QFile` 写法),
再从文件读回来解析,构成一个完整的"落盘 JSON 配置"流程;
- 故意构造一段格式错误的 JSON 字符串喂给 `fromJson`,观察 `parsed.isNull()`
`true` 时如何做错误提示(而不是让程序崩溃或读到脏数据)。
## 参考
- 本仓库对照:[`p03/ch11/qfile_basic.cpp`](../../../../p03/ch11/qfile_basic.cpp)、
[`qtextstream_io.cpp`](../../../../p03/ch11/qtextstream_io.cpp)
- Qt 官方文档:*JSON Support in Qt*doc.qt.io,选读·需联网)
@@ -0,0 +1,49 @@
// ============================================================================
// qt_json_parse_write —— 教学补充示例(非 wiki 生成,手工维护)
// 用途:方案书「C++方向 Day3」要求的 JSON 等主流数据格式解析,仓库 p03/ch11
// 原有 wiki 抓取内容只覆盖 QFile/QTextStream/QDataStream,没有 JSON
// 故补一个最小示例。
// 说明:JSON 读写不需要 GUI,用 QCoreApplication 即可,不需要
// QT_QPA_PLATFORM=offscreen,直接 tools/run_qt.sh qt_json_parse_write 即可。
// 配套文档:docs/teaching/examples/json_io/README.md
// ============================================================================
#include <QCoreApplication>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>
#include <QDebug>
int main(int argc, char* argv[]) {
QCoreApplication app(argc, argv);
// 1) 构造一个 JSON 对象(对应课程里“学员信息”这类结构)
QJsonObject student;
student["name"] = QStringLiteral("张三");
student["score"] = 92;
QJsonArray tags;
tags.append(QStringLiteral("Qt"));
tags.append(QStringLiteral("C++"));
student["tags"] = tags;
// 2) 序列化:QJsonObject -> QJsonDocument -> 字节数组(可直接写文件)
QJsonDocument doc(student);
QByteArray bytes = doc.toJson(QJsonDocument::Indented);
qDebug().noquote() << "序列化结果:\n" << bytes;
// 3) 反序列化:字节数组 -> QJsonDocument -> 取值
QJsonDocument parsed = QJsonDocument::fromJson(bytes);
if (!parsed.isNull() && parsed.isObject()) {
QJsonObject obj = parsed.object();
qDebug() << "姓名:" << obj["name"].toString();
qDebug() << "分数:" << obj["score"].toInt();
const QJsonArray parsedTags = obj["tags"].toArray();
for (const QJsonValue& v : parsedTags)
qDebug() << "标签:" << v.toString();
} else {
qDebug() << "JSON 解析失败";
return 1;
}
return 0;
}
@@ -0,0 +1,49 @@
# Model/View 模型视图架构 —— 教学补充示例
> 对应方案书「C++方向 Day3」:"高级控件应用与 Model/View 模型视图架构"。
> 仓库 `p03/` 的 wiki 抓取内容里没有 Model/View 相关代码,本示例是手工补写的
> 教学补充,**不属于** `tools/gen_part3.py` 的生成产物。
## 概念
Qt 的 Model/View 架构把"数据"和"展示"彻底解耦成三层:
- **Model**(模型):只管数据,不知道自己会被怎么展示。本示例用现成的
`QStandardItemModel`(也可以继承 `QAbstractListModel`/`QAbstractTableModel`
自己实现,工作量更大,教学上先用现成模型建立概念);
- **View**(视图):只管展示和交互,不持有数据副本,展示什么完全来自
`setModel()` 绑的那个模型。本示例用 `QListView`
- **Selection Model**(选中状态):视图的选中状态也被抽成独立对象
`QItemSelectionModel`,可以多个视图共享同一个选中状态。
对比"没有 Model/View"的写法(比如直接往 `QListWidget` 里塞 `addItem()`):数据
和展示是绑死的;Model/View 的好处在于同一份数据可以同时喂给
`QListView`/`QTableView`/`QComboBox` 等多个视图,改数据所有视图自动同步。
## 代码走读
见 [`qt_model_view_basics.cpp`](qt_model_view_basics.cpp)
1. 建一个 `QStandardItemModel`,塞 4 行字符串数据;
2. 建一个 `QListView``setModel()` 绑定上面的模型——视图本身不存数据;
3. 监听 `view->selectionModel()->currentChanged` 信号,选中项变化时更新一个
`QLabel`,体现"视图状态变化 → 通知外部"的解耦方式;
4. 代码末尾手动 `setCurrentIndex()` 选中第一行,是为了离屏自动化验证时也能
触发一次 `currentChanged`(真实交互中这一步由用户点击触发)。
## 验证方式
```bash
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_model_view_basics
```
## 练习方向(供任务卡引用)
-`QListView` 换成 `QTableView`,模型不变,观察"同一份模型换个视图"是怎么
做到的;
- 增加一个"删除选中行"按钮,调用 `model->removeRow(view->currentIndex().row())`
体会数据变化后视图自动刷新(不需要手动重画)。
## 参考
- Qt 官方文档:*Model/View Programming*doc.qt.io,选读·需联网)
@@ -0,0 +1,61 @@
// ============================================================================
// qt_model_view_basics —— 教学补充示例(非 wiki 生成,手工维护)
// 用途:方案书「C++方向 Day3」要求的 Model/View 模型视图架构,仓库 p03/ 原有
// wiki 抓取内容未覆盖这一点,故补一个最小示例。
// 配套文档:docs/teaching/examples/model_view/README.md
// 离屏验证:QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_model_view_basics
// ============================================================================
#include <QApplication>
#include <QWidget>
#include <QListView>
#include <QLabel>
#include <QVBoxLayout>
#include <QStandardItemModel>
#include <QTimer>
#include <QItemSelectionModel>
class ModelViewDemo : public QWidget {
Q_OBJECT
public:
explicit ModelViewDemo(QWidget* parent = nullptr) : QWidget(parent) {
// 模型:数据本身,与展示方式无关
auto* model = new QStandardItemModel(this);
model->appendRow(new QStandardItem(QStringLiteral("需求分析")));
model->appendRow(new QStandardItem(QStringLiteral("项目设计")));
model->appendRow(new QStandardItem(QStringLiteral("编码实现")));
model->appendRow(new QStandardItem(QStringLiteral("测试及回归")));
// 视图:只负责展示,不持有数据
auto* view = new QListView(this);
view->setModel(model);
auto* selectedLabel = new QLabel(QStringLiteral("当前选中:(无)"), this);
// 选中项变化 → 视图层通知 → 界面同步更新,模型和视图之间没有直接耦合
connect(view->selectionModel(), &QItemSelectionModel::currentChanged,
this, [selectedLabel](const QModelIndex& current, const QModelIndex&) {
if (current.isValid())
selectedLabel->setText(QStringLiteral("当前选中:%1").arg(current.data().toString()));
});
auto* layout = new QVBoxLayout(this);
layout->addWidget(view);
layout->addWidget(selectedLabel);
// 离屏自动化验证:选中第一行,触发一次 currentChanged
view->setCurrentIndex(model->index(0, 0));
}
};
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
ModelViewDemo w;
w.resize(280, 220);
w.show();
// 离屏/自动化验证:200ms 后自动退出(GUI 模式下不阻塞)
QTimer::singleShot(200, &app, &QApplication::quit);
return app.exec();
}
#include "qt_model_view_basics.moc"
@@ -0,0 +1,51 @@
# QPropertyAnimation 动画框架 —— 教学补充示例
> 对应方案书「C++方向 Day4」:"QT 动画框架(QPropertyAnimation)与状态机实战"
> 里的动画部分(状态机部分见 [`../state_machine/`](../state_machine/README.md))。
> 仓库 `p03/` 的 wiki 抓取内容里没有动画相关代码,本示例是手工补写的教学补充,
> **不属于** `tools/gen_part3.py` 的生成产物。
## 概念
`QPropertyAnimation` 能直接对 Qt 的**属性系统**`Q_PROPERTY`,比如
`geometry``windowOpacity``pos`)做插值动画,不需要手写定时器 + 每帧计算
坐标/透明度再手动调用 `update()`。三个关键参数:
- `setStartValue()` / `setEndValue()` —— 起止值;
- `setDuration()` —— 动画时长(毫秒);
- `setEasingCurve()` —— 缓动曲线,控制"匀速/加速/回弹"等运动节奏
`QEasingCurve` 提供几十种预设曲线)。
动画对象默认不会自动播放,需要显式 `start()``start(QAbstractAnimation::DeleteWhenStopped)`
可以让动画播放完自动 `delete` 自己,不用手动管理生命周期。
## 代码走读
见 [`qt_property_animation.cpp`](qt_property_animation.cpp)
1. 一个按钮初始位置在左上角 `QRect(10, 10, 100, 30)`
2. `QPropertyAnimation` 绑定按钮的 `"geometry"` 属性,800ms 内从起始矩形动画到
右下角的 `QRect(180, 140, 120, 40)`(位置和大小同时变化);
3. `QEasingCurve::OutBounce` 让动画结束时有一个"回弹"的视觉效果,
直观体现"缓动曲线改变的是运动节奏,不是起止值";
4. 离屏验证等待 1100ms(比动画时长 800ms 多留 300ms 余量)再退出,确保动画
真的播放完整。
## 验证方式
```bash
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_property_animation
```
有 X 环境直接运行可以看到按钮从左上角带回弹效果移动到右下角。
## 练习方向(供任务卡引用)
-`QEasingCurve::OutBounce` 换成 `QEasingCurve::Linear`/`InOutQuad`
对比运动节奏的差异;
- 再加一个 `QPropertyAnimation` 动画 `windowOpacity`(从 1.0 到 0.3),用
`QParallelAnimationGroup` 让位置动画和透明度动画同时播放。
## 参考
- Qt 官方文档:*Animation Framework*、*QPropertyAnimation*doc.qt.io,选读·需联网)
@@ -0,0 +1,38 @@
// ============================================================================
// qt_property_animation —— 教学补充示例(非 wiki 生成,手工维护)
// 用途:方案书「C++方向 Day4」要求的 QPropertyAnimation 动画框架,仓库 p03/
// 原有 wiki 抓取内容未覆盖这一点,故补一个最小示例。
// 配套文档:docs/teaching/examples/property_animation/README.md
// 离屏验证:QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_property_animation
// ============================================================================
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QPropertyAnimation>
#include <QTimer>
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QWidget window;
window.resize(320, 200);
auto* button = new QPushButton(QStringLiteral("看我移动"), &window);
button->setGeometry(10, 10, 100, 30);
// QPropertyAnimation 直接驱动 Qt 属性系统里的 geometry 属性,
// 不需要手写定时器 + 逐帧计算坐标。
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);
window.show();
// 离屏/自动化验证:动画时长 800ms,额外等 300ms 确保播放完再退出
QTimer::singleShot(1100, &app, &QApplication::quit);
return app.exec();
}
@@ -0,0 +1,47 @@
# QSS 界面美化 —— 教学补充示例
> 对应方案书「C++方向 Day2」:"基础 UI 控件、布局管理与 QSS 界面美化"。
> 仓库 `p03/` 的 wiki 抓取内容里没有 QSS 相关代码,本示例是手工补写的教学补充,
> **不属于** `tools/gen_part3.py` 的生成产物,重新生成 `p01`/`p03` 不会影响它。
## 概念
QSSQt Style Sheets)是 Qt 提供的类 CSS 样式语言,用来把"控件长什么样"和"控件
做什么"分离开:
- 类型选择器:`QPushButton { ... }` —— 命中所有该类型控件;
- ID 选择器:`QPushButton#okButton { ... }` —— 通过 `setObjectName()` 精确命中
某一个控件;
- 伪状态:`:hover``:pressed``:disabled` 等 —— 不用手写事件处理就能做交互反馈;
- 既可以调用 `qApp->setStyleSheet(...)` 全局生效,也可以 `widget->setStyleSheet(...)`
只作用于这个控件和它的子控件。
## 代码走读
见 [`qt_qss_styling.cpp`](qt_qss_styling.cpp)
1. 三个控件(标题 `QLabel`、确定/取消两个 `QPushButton`)分别设置 `objectName`
2. 整个窗口调一次 `setStyleSheet()`,用 ID 选择器分别美化每个控件,
`okButton`/`cancelButton` 还各自定义了 `:hover`/`:pressed` 效果;
3. 不改变任何业务逻辑(没有新增信号槽),只改变外观——这正是 QSS 的定位:
与逻辑正交。
## 验证方式
```bash
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_qss_styling # 离屏跑通,200ms 自动退出
tools/run_qt.sh qt_qss_styling # 有 X 环境可直接看窗口
```
## 练习方向(供任务卡引用)
-`okButton`/`cancelButton` 换成类型选择器 `QPushButton { ... }` 一次性美化,
对比 ID 选择器和类型选择器的适用场景差异;
-`title` 加一个 `:hover` 伪状态(`QLabel` 默认不接收鼠标事件,需要先
`setAttribute(Qt::WA_Hover)` 之类的前置条件——留给学员自己踩一次坑)。
## 参考
- 本仓库对照:[`p03/ch08/custom_widget.cpp`](../../../../p03/ch08/custom_widget.cpp)
(展示了 `QHBoxLayout` 布局管理,可与本示例的样式部分组合练习)
- Qt 官方文档:*Qt Style Sheets Reference*doc.qt.io,选读·需联网)
@@ -0,0 +1,58 @@
// ============================================================================
// qt_qss_styling —— 教学补充示例(非 wiki 生成,手工维护)
// 用途:方案书「C++方向 Day2」要求的 QSS 界面美化,仓库 p03/ 原有 wiki 抓取内容
// 未覆盖这一点,故补一个最小示例。
// 配套文档:docs/teaching/examples/qss_styling/README.md
// 离屏验证:QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_qss_styling
// ============================================================================
#include <QApplication>
#include <QWidget>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QTimer>
class StyledPanel : public QWidget {
Q_OBJECT
public:
explicit StyledPanel(QWidget* parent = nullptr) : QWidget(parent) {
auto* title = new QLabel(QStringLiteral("QSS 样式演示"), this);
title->setObjectName("title");
auto* okBtn = new QPushButton(QStringLiteral("确定"), this);
okBtn->setObjectName("okButton");
auto* cancelBtn = new QPushButton(QStringLiteral("取消"), this);
cancelBtn->setObjectName("cancelButton");
auto* layout = new QVBoxLayout(this);
layout->addWidget(title);
layout->addWidget(okBtn);
layout->addWidget(cancelBtn);
// 整个窗口一份样式表:按 objectName 选择器精确命中控件,
// 也可以只用类型选择器(QPushButton { ... })一次性套所有按钮。
setStyleSheet(
"QWidget { background-color: #f4f6f8; }"
"QLabel#title { font-size: 18px; font-weight: bold; color: #2b6cb0; padding: 6px; }"
"QPushButton { border-radius: 6px; padding: 6px 14px; color: white; }"
"QPushButton#okButton { background-color: #2f855a; }"
"QPushButton#okButton:hover { background-color: #276749; }"
"QPushButton#cancelButton { background-color: #a0522d; }"
"QPushButton#cancelButton:pressed { background-color: #7b3f22; }"
);
}
};
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
StyledPanel panel;
panel.resize(240, 160);
panel.show();
// 离屏/自动化验证:200ms 后自动退出(GUI 模式下不阻塞)
QTimer::singleShot(200, &app, &QApplication::quit);
return app.exec();
}
#include "qt_qss_styling.moc"
@@ -0,0 +1,56 @@
# QThread 多线程 —— 教学补充示例
> 对应方案书「C++方向 Day4」:"QT 多线程编程(QThread、互斥锁与防止 UI 阻塞机制)"。
> 仓库 `p03/` 的 wiki 抓取内容里没有多线程相关代码,本示例是手工补写的教学
> 补充,**不属于** `tools/gen_part3.py` 的生成产物。
## 概念
Qt 官方推荐的多线程模式是 **worker-object + moveToThread**,而不是继承
`QThread` 重写 `run()`(后者是早期教程常见但容易踩坑的写法:很容易在错误的
线程里调用槽函数)。核心步骤:
1. 把耗时逻辑写成一个普通 `QObject` 子类(`Worker`),逻辑放在一个槽函数里;
2. 创建一个 `QThread` 对象,调用 `worker.moveToThread(&thread)`——这一步之后,
`worker` 的槽函数会在 `thread` 里执行,而不是在创建它的线程里;
3. 用信号槽驱动流程:`thread.started` 触发 `worker.process()`
`worker.resultReady` 触发主线程里的回调;
4. 关键点:**跨线程的信号槽连接默认是"排队连接"queued connection**——
Qt 会自动把回调调度回接收者所在的线程的事件循环里执行,不需要手动加锁
就能安全地把子线程算出来的结果"带回"主线程。
方案书提到的"互斥锁"用于多个线程**同时读写同一块内存**的场景;本示例里子
线程和主线程之间只通过信号槽传递数据(没有共享变量),所以不需要
`QMutex`——这本身也是一个教学点:**优先用信号槽传递数据,退而求其次才用锁**。
## 代码走读
见 [`qt_thread_worker.cpp`](qt_thread_worker.cpp)
1. `Worker::process()` 模拟一次耗时计算(循环求和),完成后 `emit resultReady(sum)`
2. `worker.moveToThread(&workerThread)` 把 worker 挪到子线程;
3. `workerThread.started``worker.process`:线程一启动就开始干活;
4. `worker.resultReady` → 主线程 lambda:打印结果,并判断当前
`QThread::currentThread()` 确实是主线程(证明排队连接真的把回调带回来了);
5. 结果打印后调用 `workerThread.quit()` 结束子线程事件循环,
`workerThread.finished` 再触发 `QCoreApplication::quit()` 结束整个程序。
## 验证方式
```bash
tools/run_qt.sh qt_thread_worker
```
预期终端输出一行"子线程计算结果:...(当前在主线程打印)"。
## 练习方向(供任务卡引用)
-`Worker::process()` 里的耗时计算换成读一个大文件/睡眠几秒,感受主线程
没有被卡住(如果这是一个真实 GUI 程序,界面依然能响应鼠标点击);
- 故意去掉 `moveToThread` 这一行,观察 `resultReady` 的回调是在哪个线程执行的
`QThread::currentThread()` 判断结果会反过来),理解 `moveToThread` 到底做了
什么。
## 参考
- Qt 官方文档:*QThread*、*Threads and QObjects*doc.qt.io,选读·需联网)
@@ -0,0 +1,53 @@
// ============================================================================
// qt_thread_worker —— 教学补充示例(非 wiki 生成,手工维护)
// 用途:方案书「C++方向 Day4」要求的 QThread 多线程编程(避免耗时任务阻塞主
// 线程/界面),仓库 p03/ 原有 wiki 抓取内容未覆盖这一点,故补一个最小示例。
// 说明:用 QCoreApplication 演示跨线程信号槽通信的核心模式(moveToThread +
// 排队连接),不需要 GUI;真实项目里把“耗时计算”换成真实业务逻辑、
// 把“打印结果”换成“更新界面控件”即可,模式不变。
// 配套文档:docs/teaching/examples/qthread_worker/README.md
// ============================================================================
#include <QCoreApplication>
#include <QThread>
#include <QObject>
#include <QDebug>
// Worker 不继承 QThread,而是被 moveToThread 挪到子线程执行——这是 Qt 官方推荐
// 的写法(继承 QThread 重写 run() 是早期常见但容易踩坑的旧写法)。
class Worker : public QObject {
Q_OBJECT
public slots:
void process() {
// 模拟耗时计算(真实场景:大文件解析、网络请求、复杂运算……)
// 注意用 qint641 加到 1000000 的和是 500000500000,超过 int 上限会溢出。
qint64 sum = 0;
for (int i = 1; i <= 1000000; ++i) sum += i;
emit resultReady(sum);
}
signals:
void resultReady(qint64 result);
};
int main(int argc, char* argv[]) {
QCoreApplication app(argc, argv);
QThread workerThread;
Worker worker;
worker.moveToThread(&workerThread);
// 子线程启动后立即开始工作
QObject::connect(&workerThread, &QThread::started, &worker, &Worker::process);
// 工作完成 → 排队连接自动把回调调度回主线程 → 打印结果 → 结束线程与事件循环
QObject::connect(&worker, &Worker::resultReady, &app, [&](qint64 result) {
qDebug() << "子线程计算结果:" << result
<< "(当前在" << (QThread::currentThread() == &workerThread ? "子线程" : "主线程") << "打印)";
workerThread.quit();
});
QObject::connect(&workerThread, &QThread::finished, &app, &QCoreApplication::quit);
workerThread.start();
return app.exec();
}
#include "qt_thread_worker.moc"
@@ -0,0 +1,58 @@
# QStateMachine 状态机 —— 教学补充示例
> 对应方案书「C++方向 Day4」:"QT 动画框架(QPropertyAnimation)与状态机实战"
> 里的状态机部分(动画部分见 [`../property_animation/`](../property_animation/README.md))。
> 仓库 `p03/` 的 wiki 抓取内容里没有状态机相关代码,本示例是手工补写的教学
> 补充,**不属于** `tools/gen_part3.py` 的生成产物。
>
> 小提示:`QStateMachine`/`QState` 属于 **QtCore** 模块,不需要额外
> `find_package`,这也是本示例能直接放进现有 Qt5::Core 依赖里的原因。
## 概念
状态机框架用"状态 + 转换条件"描述一个对象的行为,替代手写一堆 `if/else`
`switch` 去判断"现在是什么状态、该往哪个状态跳":
- `QState` —— 一个状态,可以用 `assignProperty(obj, "属性名", 值)` 声明
"进入这个状态时,某个对象的某个属性应该变成什么值"(不用手写槽函数);
- `addTransition(信号源, 信号, 目标状态)` —— 声明"某个信号触发时,从当前状态
转换到目标状态"
- `QStateMachine` —— 持有所有状态,`setInitialState()` 指定初始状态,
`start()` 启动。
对比方案书里"棋牌类游戏""售货机"这类案例常见的手写状态机(一个枚举 + 一堆
`switch(state)`):`QStateMachine` 把"状态该长什么样"`assignProperty`)和
"什么时候切换状态"`addTransition`)分开声明,状态一多,可读性明显好于手写
`switch`
## 代码走读
见 [`qt_state_machine.cpp`](qt_state_machine.cpp)
1. 定义 `redState`/`greenState` 两个 `QState`,各自用 `assignProperty` 声明
进入该状态时按钮的文本和样式;
2. `redState->addTransition(&button, &QPushButton::clicked, greenState)`
反向同理——点击按钮在两态间来回切换(红灯 → 绿灯 → 红灯 → ……);
3. `machine.setInitialState(redState)` + `machine.start()` 启动状态机;
4. 离屏验证用 `QTimer::singleShot` 模拟一次 `button.click()`,观察状态确实从
红灯切换到绿灯。
## 验证方式
```bash
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_state_machine
```
## 练习方向(供任务卡引用)
- 加一个 `yellowState`(黄灯),改成红→绿→黄→红的三态循环,体会状态数量增加
`QStateMachine` 写法的可读性优势;
- 给某个状态加一个定时器转换:`redState->addTransition(timer, &QTimer::timeout, greenState)`
实现"红灯持续 3 秒后自动变绿灯"(不需要用户点击)。
## 参考
- 本仓库对照:[`p03/ch06/modal_dialog.cpp`](../../../../p03/ch06/modal_dialog.cpp)
等对话框示例里也有"根据用户操作决定下一步界面"的朴素状态切换,可以对比
"手写 if/else" 和 "QStateMachine 声明式写法" 的差异;
- Qt 官方文档:*The State Machine Framework*doc.qt.io,选读·需联网)
@@ -0,0 +1,47 @@
// ============================================================================
// qt_state_machine —— 教学补充示例(非 wiki 生成,手工维护)
// 用途:方案书「C++方向 Day4」要求的状态机实战(QStateMachine 属于 QtCore
// 不需要额外模块),仓库 p03/ 原有 wiki 抓取内容未覆盖这一点,故补一个
// 最小示例:一个“红灯/绿灯”两态按钮,点击在两态间切换。
// 配套文档:docs/teaching/examples/state_machine/README.md
// 离屏验证:QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_state_machine
// ============================================================================
#include <QApplication>
#include <QPushButton>
#include <QStateMachine>
#include <QState>
#include <QTimer>
int main(int argc, char* argv[]) {
QApplication app(argc, argv);
QPushButton button;
button.resize(140, 40);
QStateMachine machine;
auto* redState = new QState(&machine);
auto* greenState = new QState(&machine);
// 进入某个状态时执行的动作:改文本、改样式——状态机负责“流转”,
// 具体每个状态“长什么样”交给 assignProperty/连接槽函数。
redState->assignProperty(&button, "text", QStringLiteral("红灯:禁止通行"));
redState->assignProperty(&button, "styleSheet", QStringLiteral("background-color:#c0392b;color:white;"));
greenState->assignProperty(&button, "text", QStringLiteral("绿灯:允许通行"));
greenState->assignProperty(&button, "styleSheet", QStringLiteral("background-color:#27ae60;color:white;"));
// 点击按钮触发状态切换:red --clicked--> green --clicked--> red ...
redState->addTransition(&button, &QPushButton::clicked, greenState);
greenState->addTransition(&button, &QPushButton::clicked, redState);
machine.setInitialState(redState);
machine.start();
button.show();
// 离屏/自动化验证:100ms 后模拟一次点击(red -> green),
// 再等 200ms 确认状态机正常处理完过渡后自动退出
QTimer::singleShot(100, &button, [&button]() { button.click(); });
QTimer::singleShot(300, &app, &QApplication::quit);
return app.exec();
}