d94d9175b1
- 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>
371 lines
19 KiB
HTML
371 lines
19 KiB
HTML
<!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: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>
|
||
<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:30–11: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: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>
|
||
<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_<objectName>_<signal>()</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 <target></code>
|
||
或有 X 环境直接运行);中文界面文字无乱码。</p>
|
||
</div>
|
||
<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>
|
||
|
||
</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>
|