幻灯片按 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:
+199
-285
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Day 2 · Qt 开发环境/信号槽/基础控件/布局与 QSS</title>
|
||||
<title>Day 2 · Qt 入门 / 信号槽 / 计算器案例</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,11 +15,11 @@
|
||||
|
||||
<section>
|
||||
<h1>Day 2</h1>
|
||||
<h3>Qt 开发环境 / 信号槽 / 基础控件 / 布局与 QSS</h3>
|
||||
<p class="module-badge">C++/Qt 方向实训 · 第 2 / 5 天 · 讲:练 ≈ 1:2.7</p>
|
||||
<h3>Qt 概述 / 创建项目 / 第一个程序 / 信号槽 → 计算器</h3>
|
||||
<p class="module-badge">C++/Qt 方向实训 · 第 2 / 4 个讲授日</p>
|
||||
<ul>
|
||||
<li>模块 A(3 课时):项目结构 / 第一个程序 / 信号槽</li>
|
||||
<li>模块 B(3 课时):常用控件 / 布局管理 / QSS 界面美化 → 计算器案例</li>
|
||||
<li>上午(08:30–11:00 讲 / 11:00–12:00 练):Qt 概述 · 创建 Qt 项目 · 第一个 Qt 小程序</li>
|
||||
<li>下午(13:30–16:00 讲 / 16:00–17:00 作业):信号槽机制 → 项目案例:简易计算器</li>
|
||||
</ul>
|
||||
<div class="verify-box" style="font-size:0.5em;">
|
||||
<h4>开始前准备(一次性)</h4>
|
||||
@@ -30,355 +30,270 @@ 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>
|
||||
<p class="module-badge">上午 · 讲授 08:30–11:00</p>
|
||||
<h2>Qt 概述 / 创建项目 / 第一个程序 <span class="tag tag-lecture">讲</span></h2>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>最小 Qt 程序:核心概念 <span class="tag tag-lecture">讲</span></h3>
|
||||
<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> 宏触发 moc(meta-object compiler)在编译前生成额外代码——
|
||||
信号槽、属性系统、<code>qobject_cast</code> 等运行时反射能力都靠这份生成代码实现,
|
||||
这是 Qt 自己的预处理步骤,不是普通 C++ 语法。</li>
|
||||
<li>Qt = 跨平台 C++ 应用框架:一份代码,Windows/Linux/macOS 原生编译;模块化组织——
|
||||
本课程用 <code>Core</code>(对象/容器/IO)、<code>Gui</code>、<code>Widgets</code>(桌面控件)。</li>
|
||||
<li>版本纪律:本实训统一用<b>隔离安装的 Qt 5.14.2</b>(<code>.qt514/</code>),不碰系统
|
||||
Qt——顶层 CMakeLists 有版本断言,连错版本直接 <code>FATAL_ERROR</code>。这是工程上
|
||||
"锁依赖版本"的真实示范。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:仓库根 <a class="filelink" target="_blank" href="../../../CMakeLists.txt">CMakeLists.txt</a>(版本断言一节)</small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>创建 Qt 项目(CMake 方式) <span class="tag tag-lecture">讲</span></h3>
|
||||
<pre><code class="cmake" data-trim>find_package(Qt5 5.14 REQUIRED COMPONENTS Core Gui Widgets)
|
||||
add_executable(minimal_qt_app minimal_qt_app.cpp)
|
||||
set_target_properties(minimal_qt_app PROPERTIES AUTOMOC ON)
|
||||
target_link_libraries(minimal_qt_app PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)</code></pre>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>昨天的 <code>find_package</code> 概念今天落地:CMake 按 <code>CMAKE_PREFIX_PATH</code>
|
||||
找到 Qt;<code>AUTOMOC</code> 自动跑 moc(元对象编译器)——它为 <code>Q_OBJECT</code>
|
||||
类生成信号槽所需的代码,下一页就见到。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/CMakeLists.txt">p03/CMakeLists.txt</a>、
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch02/CMakeLists.txt">p03/ch02/CMakeLists.txt</a></small></p>
|
||||
</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> 阻塞进入
|
||||
事件循环分发系统事件;<code>Q_OBJECT</code> 宏触发 moc 生成信号槽/属性系统所需的元对象代码。</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>
|
||||
<li>一个进程只能有一个 <code>QApplication</code>;忘记 <code>exec()</code> 窗口一闪而过;</li>
|
||||
<li>用了信号槽却忘 <code>Q_OBJECT</code>:报 <code>undefined reference</code>——
|
||||
<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></small></p>
|
||||
</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>
|
||||
<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>
|
||||
<li>QObject 对象树 = Qt 的<b>所有权</b>答案(接昨天"谁负责 delete"):创建控件把 parent
|
||||
传进去,parent 析构自动 delete 所有子对象——整棵树只需要管好根。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>忘记传 <code>parent</code>:控件永远不会自动释放(真实内存泄漏),也不会自动出现在
|
||||
窗口里(没有父容器就没有布局位置)。</li>
|
||||
<li>用 <code>move()</code> 手动摆放坐标的控件,一旦外层套上布局管理器,手动坐标会被
|
||||
布局在下次刷新时覆盖——这是"混用绝对定位和布局"最常见的诡异 bug 来源。</li>
|
||||
<li>忘记传 parent:对象没人管,真实内存泄漏;</li>
|
||||
<li><code>move()</code> 手动定位的控件一旦套上布局会被布局接管覆盖。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.cpp</a></small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>控件创建:两种写法 <span class="tag tag-lecture">讲</span></h3>
|
||||
<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);
|
||||
QPushButton btn("我建在栈上"); // 先声明 → 后析构
|
||||
QWidget window; // 后声明 → 先析构
|
||||
btn.setParent(&window);
|
||||
// 离开作用域:window 先析构 → 父子树 delete &btn(栈地址!)→ 崩溃
|
||||
</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>
|
||||
<li>父子树会 delete 子对象 ⇒ <b>交给父子树管的对象必须建在堆上</b>。一个对象只能有
|
||||
一个所有者:给了 parent,就不能再由栈/智能指针/手动 delete 管。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>旧的字符串语法 <code>SIGNAL(clicked())</code> 函数名/参数类型写错不会编译报错,
|
||||
只会运行时"连接失败"且经常没有提示——能用新语法就不用旧语法,编译期就能发现问题。</li>
|
||||
<li>重载信号(如右侧例子的两个 <code>hungry()</code>)不加 <code>QOverload</code>
|
||||
消歧义,编译器不知道该连哪个重载,直接报错。</li>
|
||||
<li>声明顺序反过来时这段代码"恰好不崩"——不要赌析构顺序的运气,规则只有一条。
|
||||
现场演示:<code>tools/run_qt.sh qt_pitfall_stack_widget --crash</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<>::of(&Teacher::hungry), s, QOverload<>::of(&Student::treat));
|
||||
connect(t, QOverload<QString>::of(&Teacher::hungry), s, QOverload<QString>::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>
|
||||
<p><small>复现示例:<a class="filelink" target="_blank"
|
||||
href="../examples/pitfall_stack_widget/qt_pitfall_stack_widget.cpp">examples/pitfall_stack_widget/</a>
|
||||
(<a class="filelink" target="_blank" href="../examples/pitfall_stack_widget/README.md">README</a>)</small></p>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<!-- ===================== 模块 A:练 ===================== -->
|
||||
<!-- ===================== 上午:练 ===================== -->
|
||||
<section>
|
||||
<section>
|
||||
<p class="module-badge">模块 A · 练习 95min</p>
|
||||
<h2>动手:第一个 Qt 程序 <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>minimal_qt_app</code> 与 <code>button_creation</code></dd>
|
||||
<dd>编译运行 <code>minimal_qt_app</code> 与 <code>button_creation</code>;把窗口标题和
|
||||
按钮文字改成自己的学号+姓名(<b>中文</b>——如果乱码了,记住现象,下午有一页专讲)</dd>
|
||||
<dt>验收标准</dt>
|
||||
<dd><code>QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app</code> 与
|
||||
<code>button_creation</code> 均退出码为 0,无崩溃</dd>
|
||||
<dd><code>QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app</code> 退出码 0;
|
||||
有 X 环境下能看到自己改的中文标题正常显示</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>
|
||||
|
||||
<!-- ===================== 下午:讲 ===================== -->
|
||||
<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>
|
||||
<p class="module-badge">下午 · 讲授 13:30–16:00</p>
|
||||
<h2>信号槽机制 → 计算器案例 <span class="tag tag-lecture">讲</span></h2>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>信号槽:内置 / 自定义 / lambda <span class="tag tag-lecture">讲</span></h3>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>观察者模式的 Qt 实现:发送者只 <code>emit</code>,不知道谁在听——对象间彻底解耦;</li>
|
||||
<li>新语法 <code>&Class::signal</code> 编译期类型检查;旧字符串语法
|
||||
<code>SIGNAL()/SLOT()</code> 只在运行时检查;连接方式 Direct/Queued/Auto(Day5 跨线程时回收)。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>旧语法写错<b>不报编译错误</b>,只是运行时静默连接失败;</li>
|
||||
<li>重载信号不加 <code>QOverload</code> 消歧义直接编译报错。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>出处:<a class="filelink" target="_blank" href="../../../p03/ch04/builtin_signal_slot.cpp">p03/ch04/builtin_signal_slot.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/ch04/lambda_signal_slot.cpp">lambda_signal_slot.cpp</a></small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>★ lambda 槽的生命周期与第五参数 <span class="tag tag-lecture">讲</span></h3>
|
||||
<pre><code class="cpp" data-trim data-noescape>
|
||||
connect(sender, &Sender::sig, [obj]{ obj->f(); }); // ✗ obj 销毁后仍触发
|
||||
connect(sender, &Sender::sig, obj, [obj]{ obj->f(); }); // ✓ 第五参数=context
|
||||
</code></pre>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li>第五参数(context object)决定连接跟谁的生命周期挂钩:传了 context,对象析构时
|
||||
Qt 自动断开连接。规则:<b>lambda 捕获了某个 QObject,就把它作为第五参数传进去</b>。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>不传 context:接收方销毁后 lambda 仍被调用,捕获的指针就是悬空指针——UB 可能崩、
|
||||
也可能"打印出看似正常的旧值",后者更危险(开发机正常、演示现场炸)。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>复现示例:<a class="filelink" target="_blank"
|
||||
href="../examples/pitfall_lambda_capture/qt_pitfall_lambda_capture.cpp">examples/pitfall_lambda_capture/</a>
|
||||
(<a class="filelink" target="_blank" href="../examples/pitfall_lambda_capture/README.md">README</a>,
|
||||
<code>--dangle</code> 看真实悬空访问)</small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>★ 重复 connect:点一次按钮弹两次框 <span class="tag tag-lecture">讲</span></h3>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li><code>connect</code> 每调一次<b>新增一条</b>连接,Qt 不去重——同一对信号槽连两次,
|
||||
槽就被调两次。修复:connect 收拢到构造函数只做一次(首选);或
|
||||
<code>Qt::UniqueConnection</code>(只对成员函数指针有效,lambda 防不了)。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>connect 写在会被多次执行的路径上(每次打开对话框都跑一遍的"初始化"函数)——
|
||||
现象是"点一次弹两次",第一反应查按钮,根源在连接次数。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<p><small>复现示例:<a class="filelink" target="_blank"
|
||||
href="../examples/pitfall_double_connect/qt_pitfall_double_connect.cpp">examples/pitfall_double_connect/</a>
|
||||
(<a class="filelink" target="_blank" href="../examples/pitfall_double_connect/README.md">README</a>)</small></p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h3>★ QString 与中文编码 <span class="tag tag-lecture">讲</span></h3>
|
||||
<div class="concept-box">
|
||||
<h4>核心概念</h4>
|
||||
<ul>
|
||||
<li><code>QString</code> 内部是 UTF-16,和 <code>std::string</code>(字节串)是两种东西,
|
||||
互转必须指明编码:<code>toUtf8()</code>/<code>fromUtf8()</code>;中文字面量建议
|
||||
<code>QStringLiteral("中文")</code> + 源码文件保存为 UTF-8。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="pitfall-box">
|
||||
<h4>常见的坑</h4>
|
||||
<ul>
|
||||
<li>源码不是 UTF-8、或用 <code>QString(char*)</code> 隐式转换中文字面量 → 界面乱码。
|
||||
上午改学号姓名乱码的同学:根源就在这页。Day4 读写文件时编码问题还会再来一次。</li>
|
||||
</ul>
|
||||
</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>
|
||||
<h3>案例 · 简易计算器(布局脚手架:先用后学) <span class="tag tag-lecture">讲</span></h3>
|
||||
<pre><code class="cpp" data-trim data-noescape>
|
||||
auto* grid = new QGridLayout(window); // 布局原理明天下午讲透
|
||||
grid->addWidget(btn7, 0, 0); grid->addWidget(btn8, 0, 1); // 今天先会用:
|
||||
grid->addWidget(btn9, 0, 2); grid->addWidget(btnDiv, 0, 3); // 行、列坐标摆键盘
|
||||
</code></pre>
|
||||
<div class="concept-box">
|
||||
<h4>组装思路</h4>
|
||||
<ul>
|
||||
<li>数字/运算符按钮(上午的 <code>button_creation</code>)+ 点击信号槽(刚学的三种写法)
|
||||
+ <code>QGridLayout</code> 脚手架排键盘 + <code>QLabel</code>/<code>QLineEdit</code> 显示结果。</li>
|
||||
<li>项目 = 已学知识点的组装——这是 Day5 综合案例和 Day6–10 项目的预演。</li>
|
||||
</ul>
|
||||
</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<void (QSpinBox::*)(int)>(&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>
|
||||
<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>组合 <code>button_creation.cpp</code>(数字/运算符按钮)+ <code>custom_signal_slot.cpp</code>
|
||||
(点击信号槽)+ <code>custom_widget.cpp</code>(布局排布按钮),做出一个能加减乘除的最小计算器</dd>
|
||||
<dd>组合按钮 + 信号槽 + QGridLayout 脚手架,做出能加减乘除的最小计算器</dd>
|
||||
<dt>验收标准</dt>
|
||||
<dd>至少支持两个数 + 一种运算符的计算并在 <code>QLabel</code>/<code>QLineEdit</code> 上正确显示结果;
|
||||
界面不重叠、按钮可点击</dd>
|
||||
<dd>至少支持两个数 + 一种运算符的计算并正确显示结果;界面不重叠、按钮可点击;
|
||||
connect 全部用新语法且写在构造函数里(今天的坑卡现学现用)</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>
|
||||
<dd><a class="filelink" target="_blank" href="../../../p03/ch03/button_creation.cpp">p03/ch03/button_creation.cpp</a> ·
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch04/custom_signal_slot.cpp">p03/ch04/custom_signal_slot.cpp</a> ·
|
||||
<a class="filelink" target="_blank" href="../../../p03/ch08/custom_widget.cpp">p03/ch08/custom_widget.cpp</a>(布局参考)</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</section>
|
||||
@@ -388,15 +303,14 @@ setStyleSheet(
|
||||
<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>现场演示计算器,至少完成一次正确运算 + QSS 美化。</p>
|
||||
<h4>验收</h4>
|
||||
<p>现场演示计算器完成至少一次正确运算(<code>QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target></code>
|
||||
或有 X 环境直接运行);中文界面文字无乱码。</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> ·
|
||||
<p class="footer-note"><small>钩子 · 信号槽的幕后:打开 <code>build/</code> 里真实的
|
||||
<code>moc_*.cpp</code>,看看 moc 到底生成了什么代码。<br>
|
||||
明天 Day3:QMainWindow / QDialog / 布局管理器 / 常用控件——计算器要升级成"正经软件"。</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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user