Files
scuc-qt-course/docs/teaching/slides/day2.html
T
张宗平 d94d9175b1 讲义更新:Day2 幻灯片补 Designer 表单对照章节 + Day1 PPT + property_animation gif
- day2.html:组装思路改述为「两种信号槽写法」(lambda+context 批量绑定数字/运算键,
  成员函数指针绑定等号/清空/退格);新增「番外:Qt Designer 表单版对照」章节,
  讲 .ui 表单 + uic + setupUi + connectSlotsByName 自动连接,及改 objectName 致
  on_<obj>_<sig>() 静默失败的坑;任务卡补「教师答案(不下发)」。
- day1.pptx:同步更新。
- property_animation/source.gif:属性动画示例演示 gif 资源。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:52:06 +08:00

371 lines
19 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>+ 点击信号槽(刚学的两种写法:
lambda+context 批量绑定数字/运算符键,成员函数指针绑定等号/清空/退格)
+ <code>QGridLayout</code> 脚手架排键盘 + <code>QLineEdit</code> 显示结果。</li>
<li>项目 = 已学知识点的组装——这是 Day5 综合案例和 Day6–10 项目的预演。</li>
</ul>
</div>
<p><small>教师完整参考实现(现场先跑成品,再拆代码):
<a class="filelink" target="_blank" href="../examples/calculator/qt_calculator.cpp">examples/calculator/qt_calculator.cpp</a>
<a class="filelink" target="_blank" href="../examples/calculator/README.md">README</a>
<code>QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_calculator</code> 自测 5 组用例)。
<b>课上不要直接展示源码给学生抄</b>——讲思路,学生自己实现(任务卡 2)。</small></p>
</section>
<section>
<h3>★ 番外:Qt Designer 表单版对照 <span class="tag tag-lecture"></span></h3>
<div class="concept-box">
<h4>核心概念</h4>
<ul>
<li>界面还能用 <b>Qt Designer</b> 拖控件生成 <code>.ui</code> 表单(纯 XML),
<code>uic</code> 编译期翻译成 <code>ui_xxx.h</code><code>setupUi(this)</code>
一次建好控件树——业务逻辑(<code>onDigit</code>/<code>onOperator</code>……)
与代码布局版<b>一字不差</b>,换的只是"界面从哪来"。</li>
<li><code>setupUi()</code> 内部会 <code>connectSlotsByName(this)</code>:类里若有
<code>on_&lt;objectName&gt;_&lt;signal&gt;()</code> 槽会被<b>自动连接</b>——这是
信号槽之外、Designer 工作流特有的第三种"连接方式"。</li>
</ul>
</div>
<div class="pitfall-box">
<h4>常见的坑</h4>
<ul>
<li>把表单里按钮 <code>objectName</code><code>btnClear</code> 改成
<code>btnReset</code><code>on_btnClear_clicked()</code> 不报编译错误、不报运行时
错误,只是从此再也不会被调用——静默失败坑点家族的第三个成员(前两个:旧
<code>SIGNAL()/SLOT()</code> 字符串语法、重复 <code>connect</code>)。
本参考实现故意改用显式 <code>connect()</code> 规避这个坑。</li>
</ul>
</div>
<p><small>对照实现:<a class="filelink" target="_blank"
href="../examples/calculator_designer/qt_calculator_designer.cpp">examples/calculator_designer/qt_calculator_designer.cpp</a>
+ <a class="filelink" target="_blank" href="../examples/calculator_designer/calculator_designer.ui">calculator_designer.ui</a>
<a class="filelink" target="_blank" href="../examples/calculator_designer/README.md">README</a>)——
快速原型/界面调整用 Designer 更直观;控件数量随数据动态变化(如 Day5 表格行)
必须手写代码搭建,两条路线按场景选,不是谁取代谁。</small></p>
</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>
<dt>教师答案(不下发)</dt>
<dd><a class="filelink" target="_blank" href="../examples/calculator/qt_calculator.cpp">examples/calculator/</a> ——
验收时用同一组用例(7+3、6÷0、4.5+0.5、连续按运算符)现场提问对照</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>