幻灯片按 4 天版重排:Day1 全 C++、Day3 启用 ch05/ch06、Day5 综合案例
- Day1:开训 + 全天 C++(PRETEST 摸底环节内联;★所有权/★编译 vs 链接错误新卡) - Day2:Qt 入门 + 信号槽 → 计算器(★栈控件双重释放/★lambda 第五参数/ ★重复 connect/★QString 中文编码,均链到坑点复现示例) - Day3:QMainWindow/QDialog(p03/ch05、ch06 首次排课,☆两张新卡)+ 布局/控件/QSS - Day4:事件/绘图/QTableWidget + 文件系统/JSON(★工作目录/★文件编码) - Day5:综合案例全天(student_manager 走查 + QThread 提升 + 流程规范) - index.html 同步 4+1 结构;相对链接脚本校验 0 失效
This commit is contained in:
+152
-183
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Day 4 · 2D绘图/多线程/动画框架与状态机</title>
|
||||
<title>Day 4 · 事件机制 / 绘图与表格 / 文件系统</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="stylesheet" href="reveal/reveal.css">
|
||||
<link rel="stylesheet" href="reveal/theme.css">
|
||||
@@ -15,278 +15,250 @@
|
||||
|
||||
<section>
|
||||
<h1>Day 4</h1>
|
||||
<h3>2D 图形绘制 / 多线程 / 动画框架与状态机</h3>
|
||||
<p class="module-badge">C++/Qt 方向实训 · 第 4 / 5 天 · 讲:练 ≈ 1:2.7</p>
|
||||
<h3>Qt 消息机制和事件 / 绘图事件及表格事件 / 文件系统</h3>
|
||||
<p class="module-badge">C++/Qt 方向实训 · 第 4 / 4 个讲授日</p>
|
||||
<ul>
|
||||
<li>模块 A(3 课时):2D 绘图 / 多线程</li>
|
||||
<li>模块 B(3 课时):动画框架 / 状态机 → 交互式小游戏案例</li>
|
||||
<li>上午(08:30–11:00 讲 / 11:00–12:00 练):事件循环与派发 · 事件拦截 · 绘图事件 · 表格</li>
|
||||
<li>下午(13:30–16:00 讲 / 16:00–17:00 作业):文件系统(QFile/QTextStream/QDataStream + JSON)</li>
|
||||
</ul>
|
||||
<p><small>今晚的作业「本地数据存储小工具」是明天 Day5 综合案例的直接前身。</small></p>
|
||||
</section>
|
||||
|
||||
<!-- ===================== 模块 A:讲 ===================== -->
|
||||
<!-- ===================== 上午:讲 ===================== -->
|
||||
<section>
|
||||
<section>
|
||||
<p class="module-badge">模块 A · 讲授 35min</p>
|
||||
<h2>2D 绘图 / 多线程 <span class="tag tag-lecture">讲</span></h2>
|
||||
<p class="module-badge">上午 · 讲授 08:30–11:00</p>
|
||||
<h2>事件机制 / 绘图事件 / 表格事件 <span class="tag tag-lecture">讲</span></h2>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>QPainter:由 Qt 驱动的重绘模型 <span class="tag tag-lecture">讲</span></h3>
|
||||
<h3>事件循环与派发:事件 vs 信号 <span class="tag tag-lecture">讲</span></h3>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>Qt 的绘制是"框架驱动"而非"你想画就画":所有绘制只能发生在 <code>paintEvent</code>
|
||||
里,由 Qt 在需要重绘时(窗口刚显示、被遮挡后重新露出、你主动请求)自动调用——这和直接式
|
||||
绘图(想画就立刻画到屏幕)是不同的模型。</li>
|
||||
<li>控件想主动触发重绘要调用 <code>update()</code>(异步,排队到下次事件循环处理)而不是
|
||||
直接调用 <code>paintEvent()</code>;<code>QPainter</code> 只是"在哪个设备上画"的画笔,
|
||||
画布可以是控件本身,也可以是 <code>QImage</code>/<code>QPixmap</code> 等离屏设备。</li>
|
||||
<li>Day2 的 <code>app.exec()</code> 今天拆开看:事件循环不停从队列取 <code>QEvent</code>
|
||||
派发给目标控件——<b>事件</b>是框架推给控件的通知(鼠标/键盘/重绘),<b>信号槽</b>是
|
||||
对象间的主动通知,两套机制并存不冲突。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑(明天的伏笔)</h4>
|
||||
<ul>
|
||||
<li><b>主线程做耗时操作 = 事件循环停摆 = 整个界面冻结</b>——点不动、拖不动、白屏。
|
||||
这是学员项目里最高频的"卡死"来源,正确解法(多线程)明天综合案例专门讲。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch02/minimal_qt_app.cpp">p03/ch02/minimal_qt_app.cpp</a>(<code>exec()</code>)</small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>事件拦截:重写 xxxEvent vs 事件过滤器 <span class="tag tag-lecture">讲</span></h3>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>两条拦截路径:重写 <code>mousePressEvent()</code> 等虚函数(只影响自己);
|
||||
<code>installEventFilter()</code>(一个对象统一拦截多个控件的事件)。
|
||||
事件有 <code>accept()</code>/<code>ignore()</code> 状态,决定是否继续向父控件传播。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>在 <code>paintEvent</code> 之外的地方直接 <code>new QPainter(this)</code> 画东西:
|
||||
要么无效要么未定义行为,绘制必须在 <code>paintEvent</code> 回调期间进行。</li>
|
||||
<li>数据变了却忘记调用 <code>update()</code>:界面不会自动感知"要重画了",看起来像是
|
||||
"画错了",其实是"根本没重画"。</li>
|
||||
<li>重写事件函数忘记调用基类实现(<code>QWidget::mousePressEvent(e)</code>)——
|
||||
控件默认行为悄悄丢失,现象诡异难查。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch09/event_override_customwidget.cpp">p03/ch09/event_override_customwidget.cpp</a> ·
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch09/event_filter_mainwindow.cpp">event_filter_mainwindow.cpp</a> ·
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch09/mouse_event.cpp">mouse_event.cpp</a></small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>绘图:QPainter <span class="tag tag-lecture">讲</span></h3>
|
||||
<pre><code class="cpp" data-trim data-noescape>
|
||||
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));
|
||||
}
|
||||
</code></pre>
|
||||
<p><small>所有绘制都发生在 <code>paintEvent</code> 里,由 Qt 在需要重绘时自动调用。
|
||||
出处:<a class="filelink" target="_blank"
|
||||
href="../../../p03/ch10/qpainter_basic.cpp">p03/ch10/qpainter_basic.cpp</a>;绘图设备
|
||||
(<code>QImage</code>/<code>QPixmap</code>/<code>QPicture</code>)见
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch10/paint_device_qimage.cpp">paint_device_qimage.cpp</a>、
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch10/paint_device_pixmaps.cpp">paint_device_pixmaps.cpp</a></small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>QThread:线程亲和性与 worker-object <span class="tag tag-lecture">讲</span></h3>
|
||||
<h3>绘图事件:QPainter <span class="tag tag-lecture">讲</span></h3>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>"线程亲和性"(thread affinity):每个 QObject 都归属于创建它/被
|
||||
<code>moveToThread</code> 移动到的那个线程,它的槽函数默认在所属线程执行——GUI 控件必须
|
||||
始终归属主线程,这是 Qt 线程模型的核心约束。</li>
|
||||
<li>官方推荐的是 worker-object 模式(把耗时逻辑放进一个普通 QObject,
|
||||
<code>moveToThread</code> 到工作线程),而不是继承 <code>QThread</code> 重写
|
||||
<code>run()</code>——前者能继续用信号槽通信,后者容易把线程亲和性搞混。</li>
|
||||
<li>跨线程的信号槽连接会自动变成 Queued(回顾
|
||||
<a class="filelink" target="_blank" href="day2.html">Day2</a>),结果通过事件循环
|
||||
安全地"带回"接收者所在线程,不需要手动加锁。</li>
|
||||
<li>Qt 绘制是<b>框架驱动</b>而非"想画就画":所有绘制只能发生在 <code>paintEvent()</code>
|
||||
里,由 Qt 按需调用;数据变了想重画,调 <code>update()</code>(异步排队一次重绘请求)。
|
||||
绘图设备三件套 QImage/QPixmap/QPicture 作参考。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>在工作线程里直接操作 GUI 控件(比如直接 <code>label->setText()</code>):Qt 的 GUI
|
||||
模块不是线程安全的,这是未定义行为/崩溃的常见来源——正确做法永远是发信号,排队回主线程处理。</li>
|
||||
<li><code>moveToThread</code> 要在 <code>connect</code> 和 <code>thread.start()</code>
|
||||
之前完成;对象一旦有耗时任务在跑,中途再挪线程会有竞态。</li>
|
||||
<li>在 <code>paintEvent</code> 之外直接构造 QPainter 画窗口——未定义行为;</li>
|
||||
<li>数据变了忘调 <code>update()</code>——界面不会自动重画。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch10/qpainter_basic.cpp">p03/ch10/qpainter_basic.cpp</a>
|
||||
(设备类参考:<a class="filelink" target="_blank" href="../../../p03/ch10/paint_device_qimage.cpp">paint_device_qimage.cpp</a> 等)</small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>QThread 多线程(教学补充) <span class="tag tag-lecture">讲</span></h3>
|
||||
<pre><code class="cpp" data-trim data-noescape>
|
||||
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);
|
||||
</code></pre>
|
||||
<h3>表格:QTableWidget 与单元格事件 <span class="tag tag-lecture">讲</span></h3>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>item 类控件:数据直接塞进控件(<code>QTableWidgetItem</code>,new 出来交给表格
|
||||
接管所有权),上手快、中小项目够用;"表格事件"以信号暴露:
|
||||
<code>cellClicked</code> / <code>cellDoubleClicked</code> / <code>itemChanged</code>。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="concept-box">
|
||||
<h4>钩子 · 它背后是完整的 Model/View 架构</h4>
|
||||
<ul>
|
||||
<li>数据(model)与展示(view)彻底分离、同一模型可喂多个视图(QListView/QTableView);
|
||||
改模型必须发 <code>dataChanged</code> 等信号视图才会刷新。数据规模大/多视图联动时的
|
||||
正解,自学入口:<a class="filelink" target="_blank"
|
||||
href="../examples/model_view/qt_model_view_basics.cpp">examples/model_view/</a>。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:<a class="filelink" target="_blank"
|
||||
href="../examples/qthread_worker/qt_thread_worker.cpp">docs/teaching/examples/qthread_worker/qt_thread_worker.cpp</a>
|
||||
(<a class="filelink" target="_blank" href="../examples/qthread_worker/README.md">配套 README</a>)——
|
||||
仓库 <code>p03/</code> 的 wiki 抓取内容没有多线程,这是本轮补写的教学补充示例,已验证跑通。</small></p>
|
||||
href="../examples/table_widget/qt_table_widget_basics.cpp">examples/table_widget/qt_table_widget_basics.cpp</a>
|
||||
(<a class="filelink" target="_blank" href="../examples/table_widget/README.md">README</a>)</small></p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- ===================== 模块 A:练 ===================== -->
|
||||
<!-- ===================== 上午:练 ===================== -->
|
||||
<section>
|
||||
<section>
|
||||
<p class="module-badge">模块 A · 练习 95min</p>
|
||||
<h2>动手:绘图 + 多线程 <span class="tag tag-practice">练</span></h2>
|
||||
<p class="module-badge">上午 · 课堂练习 11:00–12:00(工程师指导、答疑)</p>
|
||||
<h2>动手:拦事件 + 画图形 <span class="tag tag-practice">练</span></h2>
|
||||
</section>
|
||||
<section>
|
||||
<div class="task-card">
|
||||
<h4>任务卡 1 · 画一个简单图形界面 <span class="tag tag-practice">练</span></h4>
|
||||
<h4>任务卡 1 · 事件过滤器 + 自绘图形 <span class="tag tag-practice">练</span></h4>
|
||||
<dl class="task-spec">
|
||||
<dt>目标</dt>
|
||||
<dd>仿 <code>qpainter_basic.cpp</code>,在 <code>paintEvent</code> 里画一个组合图形
|
||||
(如棋盘格/仪表盘刻度)</dd>
|
||||
<dd>① 给一个控件装事件过滤器,拦截鼠标按下并打印坐标;
|
||||
② 在 <code>paintEvent</code> 里画一个组合图形(棋盘格或仪表盘刻度)</dd>
|
||||
<dt>验收标准</dt>
|
||||
<dd>编译通过,离屏或 X 环境下能看到图形正确绘制(可截图验证)</dd>
|
||||
<dd>点击控件终端能看到坐标输出;图形正确绘制(可截图验证)</dd>
|
||||
<dt>基础知识 · 参考</dt>
|
||||
<dd><a class="filelink" target="_blank" href="../../../p03/ch10/qpainter_basic.cpp">p03/ch10/qpainter_basic.cpp</a></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="task-card">
|
||||
<h4>任务卡 2 · 感受多线程不卡界面 <span class="tag tag-practice">练</span></h4>
|
||||
<dl class="task-spec">
|
||||
<dt>目标</dt>
|
||||
<dd>把 <code>qt_thread_worker.cpp</code> 里的耗时计算换成一个更长的循环或
|
||||
<code>QThread::msleep</code>,观察主线程/事件循环没有被卡住</dd>
|
||||
<dt>验收标准</dt>
|
||||
<dd>能说清楚"如果把这段计算直接放主线程会发生什么"(界面无响应),并解释
|
||||
<code>moveToThread</code> 起的作用</dd>
|
||||
<dt>基础知识 · 参考</dt>
|
||||
<dd><a class="filelink" target="_blank" href="../examples/qthread_worker/qt_thread_worker.cpp">qt_thread_worker.cpp</a> ·
|
||||
<a class="filelink" target="_blank" href="../examples/qthread_worker/README.md">README</a></dd>
|
||||
<dd><a class="filelink" target="_blank" href="../../../p03/ch09/event_filter_mainwindow.cpp">p03/ch09/event_filter_mainwindow.cpp</a> ·
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch10/qpainter_basic.cpp">p03/ch10/qpainter_basic.cpp</a></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- ===================== 模块 B:讲 ===================== -->
|
||||
<!-- ===================== 下午:讲 ===================== -->
|
||||
<section>
|
||||
<section>
|
||||
<p class="module-badge">模块 B · 讲授 35min</p>
|
||||
<h2>动画框架 / 状态机 <span class="tag tag-lecture">讲</span></h2>
|
||||
<p class="module-badge">下午 · 讲授 13:30–16:00</p>
|
||||
<h2>文件系统:QFile / 流 / JSON <span class="tag tag-lecture">讲</span></h2>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>QPropertyAnimation:动画建立在属性系统上 <span class="tag tag-lecture">讲</span></h3>
|
||||
<h3>QIODevice 统一读写抽象 <span class="tag tag-lecture">讲</span></h3>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>Qt 的属性系统(<code>Q_PROPERTY</code>,同样靠
|
||||
<a class="filelink" target="_blank" href="day2.html">Day2</a> 讲过的 moc 生成)给每个属性配了
|
||||
统一的 get/set 接口;动画框架的本质就是"在一段时间内,用缓动曲线不断调用某个属性的
|
||||
setter",所以任何声明成 <code>Q_PROPERTY</code> 的属性(不只是内置的 geometry/opacity)
|
||||
都能被动画驱动。</li>
|
||||
<li><code>QEasingCurve</code> 决定的是"数值随时间变化的曲线形状"(线性/回弹/加减速……),
|
||||
动画本身只是"定时器 + 插值 + 调 setter"的组合,理解这点能解释为什么几乎所有属性都能做动画。</li>
|
||||
<li><code>QFile</code>/<code>QTextStream</code>/<code>QDataStream</code> 都建立在
|
||||
<code>QIODevice</code> 统一接口上——之后遇到网络/串口也是同一套;
|
||||
<code>QTextStream</code> 面向文本行,<code>QDataStream</code> 面向结构化二进制。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>要动画化的属性必须是真正的 <code>Q_PROPERTY</code>(有 setter),随手传一个字符串
|
||||
属性名如果对应类没有声明该属性,动画不会报错,只是什么也不会发生。</li>
|
||||
<li>动画作用的对象在动画播放完成前被销毁会导致悬空访问;
|
||||
<code>QAbstractAnimation::DeleteWhenStopped</code> 能让动画结束后自动释放自身,但不管
|
||||
目标对象的生命周期,目标对象仍需自己保证不提前销毁。</li>
|
||||
<li><code>open()</code> 返回值不检查——文件不存在/没权限时<b>静默失败</b>,
|
||||
后续读写全是空操作;</li>
|
||||
<li>文本/二进制模式混淆会破坏内容。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch11/qfile_basic.cpp">p03/ch11/qfile_basic.cpp</a> ·
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch11/qtextstream_io.cpp">qtextstream_io.cpp</a> ·
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch11/qdatastream_io.cpp">qdatastream_io.cpp</a></small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>QPropertyAnimation 动画(教学补充) <span class="tag tag-lecture">讲</span></h3>
|
||||
<pre><code class="cpp" data-trim data-noescape>
|
||||
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);
|
||||
</code></pre>
|
||||
<p><small>出处:<a class="filelink" target="_blank"
|
||||
href="../examples/property_animation/qt_property_animation.cpp">docs/teaching/examples/property_animation/qt_property_animation.cpp</a>
|
||||
(<a class="filelink" target="_blank" href="../examples/property_animation/README.md">配套 README</a>)</small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>QStateMachine:状态机建立在信号槽上 <span class="tag tag-lecture">讲</span></h3>
|
||||
<h3>JSON 解析与写入 <span class="tag tag-lecture">讲</span></h3>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>状态机解决的是"同一个控件在不同状态下行为/外观不同"的问题——与其用一堆
|
||||
<code>bool</code> 标志位加 if-else 判断当前处于什么状态,不如让"状态"本身成为一等对象
|
||||
(<code>QState</code>),每个状态自己声明"进入这个状态要怎样"(<code>assignProperty</code>,
|
||||
又用到了 Qt 属性系统)。</li>
|
||||
<li>状态之间的切换(<code>addTransition</code>)本质是"信号触发的转移"——转移条件就是
|
||||
一个信号,这让状态机和信号槽自然衔接:任何能发信号的事件都能驱动状态切换。</li>
|
||||
<li>序列化/反序列化是一对互逆操作,正确性标准是 <b>round-trip</b>:写出去再读回来
|
||||
数据一致;<code>QJsonDocument/Object/Array/Value</code> 四件套,
|
||||
<code>QJsonValue</code> 是能装多种类型的变体。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>忘记 <code>machine.setInitialState(...)</code> 或忘记调用 <code>machine.start()</code>:
|
||||
状态机对象存在但从未真正运行,所有 <code>assignProperty</code>/<code>addTransition</code>
|
||||
都不会生效。</li>
|
||||
<li>某个状态没有覆盖所有可能触发的信号/条件,会导致状态机"卡住"——设计状态机前先把所有
|
||||
状态和转移条件画成图,比直接写代码更不容易漏。</li>
|
||||
<li>不检查 <code>QJsonParseError</code>:格式坏的输入<b>静默变成空文档</b>;</li>
|
||||
<li>类型不匹配时 <code>toInt()</code>/<code>toString()</code> 不报错只返回默认值——
|
||||
数据悄悄丢,很难查。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:<a class="filelink" target="_blank"
|
||||
href="../examples/json_io/qt_json_parse_write.cpp">examples/json_io/qt_json_parse_write.cpp</a>
|
||||
(<a class="filelink" target="_blank" href="../examples/json_io/README.md">README</a>)</small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>★ 相对路径与工作目录 <span class="tag tag-lecture">讲</span></h3>
|
||||
<pre><code class="cpp" data-trim data-noescape>
|
||||
QFile bad("data.json"); // ✗ 相对当前工作目录——它在哪取决于你怎么启动
|
||||
QFile good(QCoreApplication::applicationDirPath() + "/data.json"); // ✓ 锚定可执行文件位置
|
||||
</code></pre>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>相对路径基于<b>进程当前工作目录</b>——不是源码目录,也不一定是可执行文件目录;
|
||||
IDE、终端、脚本三种启动方式的工作目录可能各不相同。正解:
|
||||
<code>applicationDirPath()</code> 拼路径,或只读资源走 <code>.qrc</code>(昨天的资源卡)。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>"IDE 里能读到文件,命令行跑就说不存在"——几乎每届学员必问,根源就是这页。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>QStateMachine 状态机(教学补充) <span class="tag tag-lecture">讲</span></h3>
|
||||
<h3>★ 文本编码再遇(文件版) <span class="tag tag-lecture">讲</span></h3>
|
||||
<pre><code class="cpp" data-trim data-noescape>
|
||||
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 默认编码随平台/区域设置而变——跨机器必须显式指定
|
||||
</code></pre>
|
||||
<p><small>出处:<a class="filelink" target="_blank"
|
||||
href="../examples/state_machine/qt_state_machine.cpp">docs/teaching/examples/state_machine/qt_state_machine.cpp</a>
|
||||
(<a class="filelink" target="_blank" href="../examples/state_machine/README.md">配套 README</a>)——
|
||||
<code>QStateMachine</code>/<code>QState</code> 属于 QtCore,不需要额外模块。以上两个
|
||||
(动画、状态机)都是仓库 <code>p03/</code> 原本没有的教学补充示例,已离屏验证跑通。</small></p>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>Day2 界面乱码(QString/源码编码)和今天文件乱码是<b>同一根源</b>的两次出现:
|
||||
字节 ↔ 文字之间永远隔着一层"编码",跨边界(源码/文件/网络)就必须显式指定。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>Windows 记事本写的文件 Linux 程序读出乱码(或反过来)——不是"文件坏了",
|
||||
是两边默认编码不同;养成条件反射:<b>编码要显式指定</b>。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- ===================== 模块 B:练(交互式小游戏案例,选做) ===================== -->
|
||||
<!-- ===================== 下午:作业 ===================== -->
|
||||
<section>
|
||||
<section>
|
||||
<p class="module-badge">模块 B · 练习 95min(交互式小游戏案例,练习任务,选做,非预建方案)</p>
|
||||
<h2>动手:动画 + 状态机小游戏 <span class="tag tag-practice">练</span></h2>
|
||||
<p class="module-badge">下午 · 课堂作业 16:00–17:00(工程师答疑)</p>
|
||||
<h2>作业:本地数据存储小工具 <span class="tag tag-practice">练</span></h2>
|
||||
</section>
|
||||
<section>
|
||||
<div class="task-card">
|
||||
<h4>任务卡 1 · 动画 + 状态机组合 <span class="tag tag-practice">练</span></h4>
|
||||
<h4>任务卡 2 · 本地数据存储小工具 <span class="tag tag-practice">练</span></h4>
|
||||
<dl class="task-spec">
|
||||
<dt>目标</dt>
|
||||
<dd>把动画和状态机组合到同一个窗口——例如按钮在"红灯"状态时禁止移动,"绿灯"状态时
|
||||
点击才触发动画</dd>
|
||||
<dd>设计「学员信息」结构(姓名/成绩/标签),支持增加记录并保存到 JSON 文件,
|
||||
重启程序后能读出之前保存的全部记录</dd>
|
||||
<dt>验收标准</dt>
|
||||
<dd>状态切换正确(红灯不触发动画,绿灯触发动画);动画播放完整不报错</dd>
|
||||
<dd>至少能存 2 条以上记录并在重启后完整读回;字段不丢失;数据文件路径用
|
||||
<code>applicationDirPath()</code> 拼(今天的坑卡现学现用)</dd>
|
||||
<dt>基础知识 · 参考</dt>
|
||||
<dd><a class="filelink" target="_blank" href="../examples/property_animation/">property_animation/</a> ·
|
||||
<a class="filelink" target="_blank" href="../examples/state_machine/">state_machine/</a></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<div class="task-card">
|
||||
<h4>任务卡 2(选做)· 简易反应力小游戏 <span class="tag tag-practice">练</span></h4>
|
||||
<dl class="task-spec">
|
||||
<dt>目标</dt>
|
||||
<dd>结合任务卡 1 + 鼠标事件(Day3 <code>mouse_event.cpp</code>)+ 多线程计时
|
||||
(Day4 <code>qt_thread_worker.cpp</code>),做一个"状态变化后限时点击"的最小交互游戏</dd>
|
||||
<dt>验收标准</dt>
|
||||
<dd>游戏至少有"开始→进行中→结束"三个可辨识状态,计时或计分逻辑不崩溃</dd>
|
||||
<dt>基础知识 · 参考</dt>
|
||||
<dd>上述三个教学补充示例 +
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch09/mouse_event.cpp">mouse_event.cpp</a></dd>
|
||||
<dd><a class="filelink" target="_blank" href="../examples/json_io/qt_json_parse_write.cpp">examples/json_io/</a> ·
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch11/qfile_basic.cpp">p03/ch11/qfile_basic.cpp</a></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
@@ -296,15 +268,12 @@ machine.start();
|
||||
<section>
|
||||
<h2>当日产出验证</h2>
|
||||
<div class="verify-box">
|
||||
<h4>现场演示</h4>
|
||||
<pre><code class="bash" data-trim>QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>
|
||||
# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口</code></pre>
|
||||
<p>分组演示动画+状态机组合小工具(任务卡 2 选做,量力而行)。</p>
|
||||
<h4>验收</h4>
|
||||
<p>现场演示「写入 → 重启 → 读回」完整闭环;讲得出 open()/ParseError 两处检查为什么不能省。</p>
|
||||
</div>
|
||||
<p class="footer-note"><small>明天 Day5:团队协同开发 / 综合案例复盘 / OpenCV(可选自学) ——
|
||||
大纲:<a class="filelink" href="../OUTLINE.md">docs/teaching/OUTLINE.md</a></small></p>
|
||||
<p><a class="filelink" href="index.html">返回目录</a> ·
|
||||
<a class="filelink" href="day3.html">← 上一天</a> ·
|
||||
<p class="footer-note"><small>明天 Day5 全天综合项目案例:学员信息管理器——今天的作业就是
|
||||
它的雏形;还要补上最后一块拼图:多线程防卡界面。</small></p>
|
||||
<p><a class="filelink" href="index.html">返回目录</a> · <a class="filelink" href="day3.html">← 上一天</a> ·
|
||||
<a class="filelink" href="day5.html">下一天 →</a></p>
|
||||
</section>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user