a6743fcea0
此前一版「讲」部分基本只是代码演示(贴代码 + 一句出处说明),没有讲透 知识点本身的原理。改为覆盖清单原则:每个知识点在大纲里至少固定两项—— 核心概念(是什么/为什么,不只是怎么写)+ 常见的坑(注意事项/误区), 课堂实际讲解深度按学员反馈现场调整,但大纲必须列全,以免漏讲或退化成 纯代码演示。 - custom.css 新增 .concept-box/.pitfall-box 卡片样式 - day1–5.html 每个知识点前插入对应卡片,内容与 OUTLINE.md 的覆盖清单一一对应
324 lines
16 KiB
HTML
324 lines
16 KiB
HTML
<!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 <target>
|
||
# 有 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>
|