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

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

416 lines
20 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 2 · Qt 开发环境/信号槽/基础控件/布局与 QSS</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 2</h1>
<h3>Qt 开发环境 / 信号槽 / 基础控件 / 布局与 QSS</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 2 / 5 天 · 讲:练 ≈ 1:2.7</p>
<ul>
<li>模块 A(3 课时):项目结构 / 第一个程序 / 信号槽</li>
<li>模块 B(3 课时):常用控件 / 布局管理 / QSS 界面美化 → 计算器案例</li>
</ul>
<div class="verify-box" style="font-size:0.5em;">
<h4>开始前准备(一次性)</h4>
<pre><code class="bash" data-trim>tools/install_qt_host.sh
cmake -S . -B build -G Ninja -DBUILD_QT_PART=ON \
-DCMAKE_PREFIX_PATH=$PWD/.qt514/5.14.2/gcc_64 -DCMAKE_BUILD_TYPE=Debug
cmake --build build</code></pre>
</div>
</section>
<!-- ===================== 模块 A:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 讲授 35min</p>
<h2>项目结构 / 信号槽 <span class="tag tag-lecture"></span></h2>
<ul>
<li>最小 Qt 程序结构</li>
<li>控件创建与显示</li>
<li>内置 / 自定义 / lambda 信号槽</li>
</ul>
</section>
<section>
<h3>最小 Qt 程序:核心概念 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><code>QApplication</code> 是整个进程唯一的"事件循环拥有者"——<code>app.exec()</code>
阻塞在这里进入事件循环,不断从操作系统取事件(鼠标/键盘/定时器/重绘请求)分发出去,
程序的"活着"就是这个循环在转,不是运行完就退出的普通程序。</li>
<li><code>Q_OBJECT</code> 宏触发 mocmeta-object compiler)在编译前生成额外代码——
信号槽、属性系统、<code>qobject_cast</code> 等运行时反射能力都靠这份生成代码实现,
这是 Qt 自己的预处理步骤,不是普通 C++ 语法。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>一个进程只能有一个 <code>QApplication</code> 实例,创建第二个直接崩溃/未定义行为。</li>
<li>忘记调用 <code>exec()</code>:窗口一闪而过就退出,因为根本没进入事件循环。</li>
<li>类里用了信号槽却忘记写 <code>Q_OBJECT</code>:编译能过,链接阶段报
<code>undefined reference</code>——报错信息完全不提示"缺宏",是新手最常踩的坑。</li>
</ul>
</div>
</section>
<section>
<h3>最小 Qt 程序:QApplication + exec() <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
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(); // 进入事件循环,程序在这里“活着”
}
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch02/minimal_qt_app.cpp">p03/ch02/minimal_qt_app.cpp</a>(注意仓库版本额外注入了
<code>QTimer::singleShot</code> 200ms 自动退出,便于离屏自动化验证,真实项目不需要这一行)</small></p>
</section>
<section>
<h3>控件创建:对象树与父子所有权 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>QObject 有一套"对象树"父子所有权模型:给控件传入 <code>parent</code> 后,
parent 被销毁时会自动 <code>delete</code> 所有子对象——这就是为什么 Qt 代码里到处是
<code>new</code> 却几乎不见 <code>delete</code></li>
<li>两种写法(先构造后设属性 / 构造时一次性传参)本质相同,父对象的作用一样。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>忘记传 <code>parent</code>:控件永远不会自动释放(真实内存泄漏),也不会自动出现在
窗口里(没有父容器就没有布局位置)。</li>
<li><code>move()</code> 手动摆放坐标的控件,一旦外层套上布局管理器,手动坐标会被
布局在下次刷新时覆盖——这是"混用绝对定位和布局"最常见的诡异 bug 来源。</li>
</ul>
</div>
</section>
<section>
<h3>控件创建:两种写法 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
// 第一种:先构造再设属性
QPushButton* btn = new QPushButton;
btn->setParent(this);
btn->setText(QStringLiteral("德玛西亚"));
btn->move(100, 100);
// 第二种:构造时指定文本与父亲
QPushButton* btn2 = new QPushButton(QStringLiteral("孙悟空"), this);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.cpp</a></small></p>
</section>
<section>
<h3>信号槽:观察者模式 + moc <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>信号槽本质是"观察者模式"的 Qt 实现:发送者不知道、也不关心谁在监听,彻底解耦发送方
和接收方——这和"直接调用某个函数"是完全不同的设计取向。</li>
<li><code>connect()</code> 背后依赖 moc 生成的元对象信息做类型检查:新语法
<code>&Class::signal</code><strong>编译期</strong>检查,字符串旧语法
<code>SIGNAL()/SLOT()</code><strong>运行时</strong>检查,容易悄悄连接失败。</li>
<li>连接方式分 Direct(同线程直接调用)/ Queued(跨线程排队,下次事件循环处理)/
Auto(默认,同线程 Direct、跨线程自动 Queued)——<a class="filelink" target="_blank"
href="day4.html">Day4</a> 讲 QThread 时还会用到。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>旧的字符串语法 <code>SIGNAL(clicked())</code> 函数名/参数类型写错不会编译报错,
只会运行时"连接失败"且经常没有提示——能用新语法就不用旧语法,编译期就能发现问题。</li>
<li>重载信号(如右侧例子的两个 <code>hungry()</code>)不加 <code>QOverload</code>
消歧义,编译器不知道该连哪个重载,直接报错。</li>
</ul>
</div>
</section>
<section>
<h3>信号槽:内置 / 自定义 / 重载消歧义 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
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&lt;&gt;::of(&Teacher::hungry), s, QOverload&lt;&gt;::of(&Student::treat));
connect(t, QOverload&lt;QString&gt;::of(&Teacher::hungry), s, QOverload&lt;QString&gt;::of(&Student::treat));
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch04/custom_signal_slot.cpp">p03/ch04/custom_signal_slot.cpp</a>
lambda 槽 + 值捕获/引用捕获对比见
<a class="filelink" target="_blank" href="../../../p03/ch04/lambda_signal_slot.cpp">lambda_signal_slot.cpp</a></small></p>
</section>
</section>
<!-- ===================== 模块 A:练 ===================== -->
<section>
<section>
<p class="module-badge">模块 A · 练习 95min</p>
<h2>动手:第一个 Qt 程序 <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>minimal_qt_app</code><code>button_creation</code></dd>
<dt>验收标准</dt>
<dd><code>QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app</code>
<code>button_creation</code> 均退出码为 0,无崩溃</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch02/minimal_qt_app.cpp">p03/ch02/minimal_qt_app.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.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>QLabel</code> 的文本内容</dd>
<dt>验收标准</dt>
<dd>编译通过;离屏或有 X 环境下点击/模拟点击后 <code>QLabel</code> 文本确实改变;
<code>connect</code> 语法而非直接函数调用</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch04/custom_signal_slot.cpp">custom_signal_slot.cpp</a>(主参考)·
<a class="filelink" target="_blank" href="../../../p03/ch04/lambda_signal_slot.cpp">lambda_signal_slot.cpp</a>lambda 槽写法对比)</dd>
</dl>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 3(选做)· 重载信号消歧义 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>仿 <code>builtin_signal_slot.cpp</code>,给一个类定义两个同名不同参的信号,用
<code>QOverload</code> 消歧义连接</dd>
<dt>验收标准</dt>
<dd>两个重载信号都能各自触发对应的槽</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch04/builtin_signal_slot.cpp">p03/ch04/builtin_signal_slot.cpp</a></dd>
</dl>
</div>
</section>
</section>
<!-- ===================== 模块 B:讲 ===================== -->
<section>
<section>
<p class="module-badge">模块 B · 讲授 35min</p>
<h2>常用控件 / 布局管理 / QSS 美化 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>资源系统:.qrc 是什么 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li><code>:/Image/boat.jpg</code> 这种路径不是文件系统路径,是 Qt 资源系统(QRC)——
图片等资源在编译期被打包进可执行文件本身,运行时不依赖外部文件是否存在,这对
"打包发布"很关键。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li><code>.qrc</code> 里资源路径/文件名和代码里 <code>:/...</code> 引用的不一致,会在
运行时静默失败(<code>QPixmap</code> 为空、图不显示),不会报编译错误。</li>
<li>忘记把 <code>.qrc</code> 接入 CMake 的 <code>qt5_add_resources</code>/AUTORCC
资源系统压根不生效。</li>
</ul>
</div>
</section>
<section>
<h3>常用控件:QLabel 显示图片 <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
QPixmap pixmap;
pixmap.load(":/Image/boat.jpg"); // 从 Qt 资源系统(.qrc)加载
QLabel* label = new QLabel(this);
label->setPixmap(pixmap);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch08/label_pixmap.cpp">p03/ch08/label_pixmap.cpp</a>
HTML 富文本标签见 <a class="filelink" target="_blank" href="../../../p03/ch08/label_text_html.cpp">label_text_html.cpp</a></small></p>
</section>
<section>
<h3>布局管理:为什么不手算坐标 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>布局管理器(<code>QLayout</code>)接管子控件的位置和大小计算,随窗口缩放自动重新
排布——这是"不要自己算坐标"的根本原因,也是跨平台/跨分辨率 UI 能正确显示的基础。</li>
<li>布局对象本身也遵循 QObject 父子所有权:<code>new QHBoxLayout(this)</code>
把布局设为 <code>this</code> 的布局,同时隐式接管了 <code>addWidget</code> 进来的子控件。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>同一个控件不能同时属于两个布局,重复 <code>addWidget</code> 到不同布局会导致界面
异常或崩溃。</li>
<li>布局设置之后又用 <code>setGeometry</code>/<code>move</code> 手动改控件位置,效果
会被布局在下次刷新时覆盖(呼应上一节"控件创建"的坑)。</li>
</ul>
</div>
</section>
<section>
<h3>自定义控件与布局:QHBoxLayout <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
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&lt;void (QSpinBox::*)(int)&gt;(&QSpinBox::valueChanged), slider, &QSlider::setValue);
connect(slider, &QSlider::valueChanged, spin, &QSpinBox::setValue);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../../../p03/ch08/custom_widget.cpp">p03/ch08/custom_widget.cpp</a>——布局管理器负责排布,
不用手算控件坐标。</small></p>
</section>
<section>
<h3>QSS:类 CSS 的样式层叠 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>QSS 语法脱胎于 CSS:选择器(类型选择器 <code>QPushButton</code>、ID 选择器
<code>#okButton</code>、伪状态 <code>:hover</code>/<code>:pressed</code>+ 属性声明,
但作用对象是 QWidget 树而不是 DOM。</li>
<li>样式表遵循层叠规则:作用在父控件上的样式默认会被子控件继承,除非子控件有自己更
具体的规则覆盖。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>用 ID 选择器(<code>#okButton</code>)必须先 <code>setObjectName("okButton")</code>
忘记设置会导致规则完全不生效且没有任何报错。</li>
<li>在某个容器上 <code>setStyleSheet</code> 会级联影响它所有子控件(包括不想改的),
范围失控是 QSS 最容易踩的坑,通常要用更具体的选择器限定范围。</li>
</ul>
</div>
</section>
<section>
<h3>QSS 界面美化(教学补充) <span class="tag tag-lecture"></span></h3>
<pre><code class="cpp" data-trim data-noescape>
okBtn->setObjectName("okButton");
setStyleSheet(
"QPushButton#okButton { background-color: #2f855a; border-radius: 6px; color: white; }"
"QPushButton#okButton:hover { background-color: #276749; }"
);
</code></pre>
<p><small>出处:<a class="filelink" target="_blank"
href="../examples/qss_styling/qt_qss_styling.cpp">docs/teaching/examples/qss_styling/qt_qss_styling.cpp</a>
<a class="filelink" target="_blank" href="../examples/qss_styling/README.md">配套 README</a>)——
仓库 <code>p03/</code> 的 wiki 抓取内容没有 QSS,这是本轮补写的教学补充示例,已离屏验证跑通。</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>组合 <code>button_creation.cpp</code>(数字/运算符按钮)+ <code>custom_signal_slot.cpp</code>
(点击信号槽)+ <code>custom_widget.cpp</code>(布局排布按钮),做出一个能加减乘除的最小计算器</dd>
<dt>验收标准</dt>
<dd>至少支持两个数 + 一种运算符的计算并在 <code>QLabel</code>/<code>QLineEdit</code> 上正确显示结果;
界面不重叠、按钮可点击</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../../../p03/ch03/button_creation.cpp">button_creation.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch04/custom_signal_slot.cpp">custom_signal_slot.cpp</a> ·
<a class="filelink" target="_blank" href="../../../p03/ch08/custom_widget.cpp">custom_widget.cpp</a></dd>
</dl>
</div>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 用 QSS 美化计算器 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>给任务卡 1 的计算器套一份 QSS 样式(数字键/运算键/等号键区分颜色)</dd>
<dt>验收标准</dt>
<dd>至少 2 类按钮有不同的 <code>background-color</code>;至少 1 个按钮有
<code>:hover</code><code>:pressed</code> 效果</dd>
<dt>基础知识 · 参考</dt>
<dd><a class="filelink" target="_blank" href="../examples/qss_styling/qt_qss_styling.cpp">qt_qss_styling.cpp</a> ·
<a class="filelink" target="_blank" href="../examples/qss_styling/README.md">README</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>现场演示计算器,至少完成一次正确运算 + QSS 美化。</p>
</div>
<p class="footer-note"><small>明天 Day3:高级控件 Model/View / 事件机制 / 文件 IO 与 JSON ——
大纲:<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="day1.html">← 上一天</a> ·
<a class="filelink" href="day3.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>