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_appbutton_creation
验收标准
QT_QPA_PLATFORM=offscreen tools/run_qt.sh minimal_qt_appbutton_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 美化。

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