# Day 2 · Qt 开发环境/信号槽/基础控件/布局与 QSS

# Day 2

### Qt 开发环境 / 信号槽 / 基础控件 / 布局与 QSS

C++/Qt 方向实训 · 第 2 / 5 天 · 讲:练 ≈ 1:2.7

- 模块 A（3 课时）：项目结构 / 第一个程序 / 信号槽
- 模块 B（3 课时）：常用控件 / 布局管理 / QSS 界面美化 → 计算器案例

#### 开始前准备（一次性）

    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

模块 A · 讲授 35min

## 项目结构 / 信号槽 讲

- 最小 Qt 程序结构
- 控件创建与显示
- 内置 / 自定义 / lambda 信号槽

### 最小 Qt 程序：核心概念 讲

#### 核心概念

- `QApplication` 是整个进程唯一的"事件循环拥有者"——`app.exec()` 阻塞在这里进入事件循环，不断从操作系统取事件（鼠标/键盘/定时器/重绘请求）分发出去， 程序的"活着"就是这个循环在转，不是运行完就退出的普通程序。
- `Q_OBJECT` 宏触发 moc（meta-object compiler）在编译前生成额外代码—— 信号槽、属性系统、`qobject_cast` 等运行时反射能力都靠这份生成代码实现， 这是 Qt 自己的预处理步骤，不是普通 C++ 语法。

#### 常见的坑

- 一个进程只能有一个 `QApplication` 实例，创建第二个直接崩溃/未定义行为。
- 忘记调用 `exec()`：窗口一闪而过就退出，因为根本没进入事件循环。
- 类里用了信号槽却忘记写 `Q_OBJECT`：编译能过，链接阶段报 `undefined reference`——报错信息完全不提示"缺宏"，是新手最常踩的坑。

### 最小 Qt 程序：QApplication + exec() 讲

    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();              // 进入事件循环，程序在这里“活着”
    }

出处：p03/ch02/minimal_qt_app.cpp（注意仓库版本额外注入了 `QTimer::singleShot` 200ms 自动退出，便于离屏自动化验证，真实项目不需要这一行）

### 控件创建：对象树与父子所有权 讲

#### 核心概念

- QObject 有一套"对象树"父子所有权模型：给控件传入 `parent` 后， parent 被销毁时会自动 `delete` 所有子对象——这就是为什么 Qt 代码里到处是 `new` 却几乎不见 `delete`。
- 两种写法（先构造后设属性 / 构造时一次性传参）本质相同，父对象的作用一样。

#### 常见的坑

- 忘记传 `parent`：控件永远不会自动释放（真实内存泄漏），也不会自动出现在 窗口里（没有父容器就没有布局位置）。
- 用 `move()` 手动摆放坐标的控件，一旦外层套上布局管理器，手动坐标会被 布局在下次刷新时覆盖——这是"混用绝对定位和布局"最常见的诡异 bug 来源。

### 控件创建：两种写法 讲

    // 第一种：先构造再设属性
    QPushButton* btn = new QPushButton;
    btn->setParent(this);
    btn->setText(QStringLiteral("德玛西亚"));
    btn->move(100, 100);

    // 第二种：构造时指定文本与父亲
    QPushButton* btn2 = new QPushButton(QStringLiteral("孙悟空"), this);

出处：p03/ch03/button_creation.cpp

### 信号槽：观察者模式 + moc 讲

#### 核心概念

- 信号槽本质是"观察者模式"的 Qt 实现：发送者不知道、也不关心谁在监听，彻底解耦发送方 和接收方——这和"直接调用某个函数"是完全不同的设计取向。
- `connect()` 背后依赖 moc 生成的元对象信息做类型检查：新语法 `&Class::signal` 是编译期检查，字符串旧语法 `SIGNAL()/SLOT()` 是运行时检查，容易悄悄连接失败。
- 连接方式分 Direct（同线程直接调用）/ Queued（跨线程排队，下次事件循环处理）/ Auto（默认，同线程 Direct、跨线程自动 Queued）——Day4 讲 QThread 时还会用到。

#### 常见的坑

- 旧的字符串语法 `SIGNAL(clicked())` 函数名/参数类型写错不会编译报错， 只会运行时"连接失败"且经常没有提示——能用新语法就不用旧语法，编译期就能发现问题。
- 重载信号（如右侧例子的两个 `hungry()`）不加 `QOverload` 消歧义，编译器不知道该连哪个重载，直接报错。

### 信号槽：内置 / 自定义 / 重载消歧义 讲

    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));

出处：p03/ch04/custom_signal_slot.cpp； lambda 槽 + 值捕获/引用捕获对比见 lambda_signal_slot.cpp

模块 A · 练习 95min

## 动手：第一个 Qt 程序 练

#### 任务卡 1 · 跑通最小程序 练

目标
  编译运行 `minimal_qt_app` 与 `button_creation`
验收标准
  `QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_app` 与 `button_creation` 均退出码为 0，无崩溃
基础知识 · 参考
  p03/ch02/minimal_qt_app.cpp · p03/ch03/button_creation.cpp

#### 任务卡 2 · 自定义信号槽改文本 练

目标
  给一个按钮连接自定义信号槽：点击后改变一个 `QLabel` 的文本内容
验收标准
  编译通过；离屏或有 X 环境下点击/模拟点击后 `QLabel` 文本确实改变； 用 `connect` 语法而非直接函数调用
基础知识 · 参考
  custom_signal_slot.cpp（主参考）· lambda_signal_slot.cpp（lambda 槽写法对比）

#### 任务卡 3（选做）· 重载信号消歧义 练

目标
  仿 `builtin_signal_slot.cpp`，给一个类定义两个同名不同参的信号，用 `QOverload` 消歧义连接
验收标准
  两个重载信号都能各自触发对应的槽
基础知识 · 参考
  p03/ch04/builtin_signal_slot.cpp

模块 B · 讲授 35min

## 常用控件 / 布局管理 / QSS 美化 讲

### 资源系统：.qrc 是什么 讲

#### 核心概念

- `:/Image/boat.jpg` 这种路径不是文件系统路径，是 Qt 资源系统（QRC）—— 图片等资源在编译期被打包进可执行文件本身，运行时不依赖外部文件是否存在，这对 "打包发布"很关键。

#### 常见的坑

- `.qrc` 里资源路径/文件名和代码里 `:/...` 引用的不一致，会在 运行时静默失败（`QPixmap` 为空、图不显示），不会报编译错误。
- 忘记把 `.qrc` 接入 CMake 的 `qt5_add_resources`/AUTORCC， 资源系统压根不生效。

### 常用控件：QLabel 显示图片 讲

    QPixmap pixmap;
    pixmap.load(":/Image/boat.jpg");   // 从 Qt 资源系统（.qrc）加载
    QLabel* label = new QLabel(this);
    label->setPixmap(pixmap);

出处：p03/ch08/label_pixmap.cpp； HTML 富文本标签见 label_text_html.cpp

### 布局管理：为什么不手算坐标 讲

#### 核心概念

- 布局管理器（`QLayout`）接管子控件的位置和大小计算，随窗口缩放自动重新 排布——这是"不要自己算坐标"的根本原因，也是跨平台/跨分辨率 UI 能正确显示的基础。
- 布局对象本身也遵循 QObject 父子所有权：`new QHBoxLayout(this)` 把布局设为 `this` 的布局，同时隐式接管了 `addWidget` 进来的子控件。

#### 常见的坑

- 同一个控件不能同时属于两个布局，重复 `addWidget` 到不同布局会导致界面 异常或崩溃。
- 布局设置之后又用 `setGeometry`/`move` 手动改控件位置，效果 会被布局在下次刷新时覆盖（呼应上一节"控件创建"的坑）。

### 自定义控件与布局：QHBoxLayout 讲

    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);

出处：p03/ch08/custom_widget.cpp——布局管理器负责排布， 不用手算控件坐标。

### QSS：类 CSS 的样式层叠 讲

#### 核心概念

- QSS 语法脱胎于 CSS：选择器（类型选择器 `QPushButton`、ID 选择器 `#okButton`、伪状态 `:hover`/`:pressed`）+ 属性声明， 但作用对象是 QWidget 树而不是 DOM。
- 样式表遵循层叠规则：作用在父控件上的样式默认会被子控件继承，除非子控件有自己更 具体的规则覆盖。

#### 常见的坑

- 用 ID 选择器（`#okButton`）必须先 `setObjectName("okButton")`， 忘记设置会导致规则完全不生效且没有任何报错。
- 在某个容器上 `setStyleSheet` 会级联影响它所有子控件（包括不想改的）， 范围失控是 QSS 最容易踩的坑，通常要用更具体的选择器限定范围。

### QSS 界面美化（教学补充） 讲

    okBtn->setObjectName("okButton");
    setStyleSheet(
        "QPushButton#okButton { background-color: #2f855a; border-radius: 6px; color: white; }"
        "QPushButton#okButton:hover { background-color: #276749; }"
    );

出处：docs/teaching/examples/qss_styling/qt_qss_styling.cpp （配套 README）—— 仓库 `p03/` 的 wiki 抓取内容没有 QSS，这是本轮补写的教学补充示例，已离屏验证跑通。

模块 B · 练习 95min（计算器案例，练习任务，非预建方案）

## 动手：做一个计算器 练

#### 任务卡 1 · 计算器雏形 练

目标
  组合 `button_creation.cpp`（数字/运算符按钮）+ `custom_signal_slot.cpp` （点击信号槽）+ `custom_widget.cpp`（布局排布按钮），做出一个能加减乘除的最小计算器
验收标准
  至少支持两个数 + 一种运算符的计算并在 `QLabel`/`QLineEdit` 上正确显示结果； 界面不重叠、按钮可点击
基础知识 · 参考
  button_creation.cpp · custom_signal_slot.cpp · custom_widget.cpp

#### 任务卡 2 · 用 QSS 美化计算器 练

目标
  给任务卡 1 的计算器套一份 QSS 样式（数字键/运算键/等号键区分颜色）
验收标准
  至少 2 类按钮有不同的 `background-color`；至少 1 个按钮有 `:hover` 或 `:pressed` 效果
基础知识 · 参考
  qt_qss_styling.cpp · README

## 当日产出验证

#### 现场演示

    QT_QPA_PLATFORM=offscreen tools/run_qt.sh <target>
    # 有 X 环境：去掉 QT_QPA_PLATFORM=offscreen 直接看窗口

现场演示计算器，至少完成一次正确运算 + QSS 美化。

返回目录 · ← 上一天 · 下一天 →