Files
scuc-qt-course/docs/teaching/slides/day2.html
T
张宗平 0f81edcc74 幻灯片按 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 失效
2026-07-05 12:49:44 +08:00

330 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 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">
<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 概述 / 创建项目 / 第一个程序 / 信号槽 → 计算器</h3>
<p class="module-badge">C++/Qt 方向实训 · 第 2 / 4 个讲授日</p>
<ul>
<li>上午(08:3011:00 讲 / 11:0012:00 练):Qt 概述 · 创建 Qt 项目 · 第一个 Qt 小程序</li>
<li>下午(13:3016:00 讲 / 16:0017:00 作业):信号槽机制 → 项目案例:简易计算器</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>
<!-- ===================== 上午:讲 ===================== -->
<section>
<section>
<p class="module-badge">上午 · 讲授 08:3011:00</p>
<h2>Qt 概述 / 创建项目 / 第一个程序 <span class="tag tag-lecture"></span></h2>
</section>
<section>
<h3>Qt 概述 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<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>;忘记 <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>控件创建与对象树 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>QObject 对象树 = Qt 的<b>所有权</b>答案(接昨天"谁负责 delete"):创建控件把 parent
传进去,parent 析构自动 delete 所有子对象——整棵树只需要管好根。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<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>
<pre><code class="cpp" data-trim data-noescape>
QPushButton btn("我建在栈上"); // 先声明 → 后析构
QWidget window; // 后声明 → 先析构
btn.setParent(&window);
// 离开作用域:window 先析构 → 父子树 delete &btn(栈地址!)→ 崩溃
</code></pre>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>父子树会 delete 子对象 ⇒ <b>交给父子树管的对象必须建在堆上</b>。一个对象只能有
一个所有者:给了 parent,就不能再由栈/智能指针/手动 delete 管。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>声明顺序反过来时这段代码"恰好不崩"——不要赌析构顺序的运气,规则只有一条。
现场演示:<code>tools/run_qt.sh qt_pitfall_stack_widget --crash</code>(真崩给你看)。</li>
</ul>
</div>
<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>
<!-- ===================== 上午:练 ===================== -->
<section>
<section>
<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>
<dl class="task-spec">
<dt>目标</dt>
<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> 退出码 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>
<p class="module-badge">下午 · 讲授 13:3016: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>&amp;Class::signal</code> 编译期类型检查;旧字符串语法
<code>SIGNAL()/SLOT()</code> 只在运行时检查;连接方式 Direct/Queued/AutoDay5 跨线程时回收)。</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>
<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>
<!-- ===================== 下午:作业 ===================== -->
<section>
<section>
<p class="module-badge">下午 · 课堂作业 16:00–17:00(工程师答疑)</p>
<h2>作业:完成计算器 <span class="tag tag-practice"></span></h2>
</section>
<section>
<div class="task-card">
<h4>任务卡 2 · 简易计算器 <span class="tag tag-practice"></span></h4>
<dl class="task-spec">
<dt>目标</dt>
<dd>组合按钮 + 信号槽 + QGridLayout 脚手架,做出能加减乘除的最小计算器</dd>
<dt>验收标准</dt>
<dd>至少支持两个数 + 一种运算符的计算并正确显示结果;界面不重叠、按钮可点击;
connect 全部用新语法且写在构造函数里(今天的坑卡现学现用)</dd>
<dt>基础知识 · 参考</dt>
<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>
</section>
<!-- ===================== 收尾 ===================== -->
<section>
<h2>当日产出验证</h2>
<div class="verify-box">
<h4>验收</h4>
<p>现场演示计算器完成至少一次正确运算(<code>QT_QPA_PLATFORM=offscreen tools/run_qt.sh &lt;target&gt;</code>
或有 X 环境直接运行);中文界面文字无乱码。</p>
</div>
<p class="footer-note"><small>钩子 · 信号槽的幕后:打开 <code>build/</code> 里真实的
<code>moc_*.cpp</code>,看看 moc 到底生成了什么代码。<br>
明天 Day3QMainWindow / 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>
</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>