Files
scuc-qt-course/docs/teaching/slides/day4.html
T
张宗平 a6743fcea0 每个「讲」知识点补齐核心概念 + 常见的坑,幻灯片与大纲同步
此前一版「讲」部分基本只是代码演示(贴代码 + 一句出处说明),没有讲透
知识点本身的原理。改为覆盖清单原则:每个知识点在大纲里至少固定两项——
核心概念(是什么/为什么,不只是怎么写)+ 常见的坑(注意事项/误区),
课堂实际讲解深度按学员反馈现场调整,但大纲必须列全,以免漏讲或退化成
纯代码演示。

- custom.css 新增 .concept-box/.pitfall-box 卡片样式
- day1–5.html 每个知识点前插入对应卡片,内容与 OUTLINE.md 的覆盖清单一一对应
2026-07-02 16:25:46 +08:00

324 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>Day 4 · 2D绘图/多线程/动画框架与状态机</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">
<link rel="stylesheet" href="reveal/plugin/highlight/monokai.css">
<link rel="stylesheet" href="custom.css">
</head>
<body>
<div class="reveal">
<div class="slides">
<section>
<h1>Day 4</h1>
<h3>2D 图形绘制 / 多线程 / 动画框架与状态机</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 4 / 5 天 · 讲:练 ≈ 1:2.7</p>
<ul>
<li>模块 A(3 课时):2D 绘图 / 多线程</li>
<li>模块 B(3 课时):动画框架 / 状态机 → 交互式小游戏案例</li>
</ul>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>2D 绘图 / 多线程 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>QPainter:由 Qt 驱动的重绘模型 <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>
</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>
</ul>
</div>
</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>
<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>
</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>
</ul>
</div>
</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>
<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>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:绘图 + 多线程 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<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>
<dt>验收标准</dt>
<dd>编译通过,离屏或 X 环境下能看到图形正确绘制(可截图验证)</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>
</dl>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>动画框架 / 状态机 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>QPropertyAnimation:动画建立在属性系统上 <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>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>要动画化的属性必须是真正的 <code>Q_PROPERTY</code>(有 setter),随手传一个字符串
属性名如果对应类没有声明该属性,动画不会报错,只是什么也不会发生。</li>
<li>动画作用的对象在动画播放完成前被销毁会导致悬空访问;
<code>QAbstractAnimation::DeleteWhenStopped</code> 能让动画结束后自动释放自身,但不管
目标对象的生命周期,目标对象仍需自己保证不提前销毁。</li>
</ul>
</div>
</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>
<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>
</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>
</ul>
</div>
</section>
<section>
<h3>QStateMachine 状态机(教学补充) <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();
</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>
</section>
</section>
<!-- ===================== 模块 B:练(交互式小游戏案例,选做) ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 练习 95min(交互式小游戏案例,练习任务,选做,非预建方案)</p>
<h2>动手:动画 + 状态机小游戏 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 1 · 动画 + 状态机组合 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>把动画和状态机组合到同一个窗口——例如按钮在"红灯"状态时禁止移动,"绿灯"状态时
点击才触发动画</dd>
<dt>验收标准</dt>
<dd>状态切换正确(红灯不触发动画,绿灯触发动画);动画播放完整不报错</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>
</dl>
</div>
</section>
</section>
<!-- ===================== 收尾 ===================== -->
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>现场演示</h4>
<pre><code class="bash" data-trim>QT_QPA_PLATFORM=offscreen tools/run_qt.sh &lt;target&gt;
# 有 X 环境:去掉 QT_QPA_PLATFORM=offscreen 直接看窗口</code></pre>
<p>分组演示动画+状态机组合小工具(任务卡 2 选做,量力而行)。</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> ·
<a class="filelink" href="day5.html">下一天 →</a></p>
</section>
</div>
</div>
<script src="reveal/reveal.js"></script>
<script src="reveal/plugin/highlight/highlight.js"></script>
<script>
Reveal.initialize({
hash: true, controls: true, progress: true, center: true, slideNumber: true,
plugins: [ RevealHighlight ]
});
</script>
</body>
</html>