Qt 控件入门:QLabel 从拖拽到对象树,一篇讲透

序章:读完能掌握

如果你刚接触 Qt,面对满屏的控件和信号槽,可能会觉得无从下手。别担心,这篇教程就是为你准备的。读完本文,你将掌握以下 4 个核心收获:

  • QLabel 的两种用法:既能用 Qt Designer 拖拽摆放,也能用代码动态创建,两种方式各有适用场景。
  • QString 的基本操作:作为 Qt 最常用的字符串类,你会学会它的构造、拼接和格式化。
  • 对象树机制:理解 Qt 如何自动管理控件内存,以及不挂到对象树会有什么后果。
  • 继承扩展 QLabel:通过一个完整 demo,亲手重写析构函数,亲眼看到对象销毁的时机。

全文约 6000 字,节奏紧凑,建议边读边动手敲代码。准备好了吗?我们开始。

一、QLabel 上场

QLabel 是 Qt 中最基础的显示控件,专门用来展示文本、图片或富文本。它本身不接收用户输入,但却是界面布局中最常用的元素之一。这一节我们分两条路来掌握它:拖控件和写代码。

1.1 拖控件:Qt Designer 快速上手

打开 Qt Creator,新建一个 Qt Widgets Application 项目。在界面设计模式下,从左侧控件栏中找到 Label ,直接拖到中央窗口上。右侧属性编辑器里,你可以修改它的 text 属性,比如改成「你好,Qt」。运行程序,文字就会显示在窗口上。

拖拽方式适合快速搭建界面原型,所见即所得,非常适合初学者理解布局。但它的缺点是:控件是写死在 .ui 文件里的,如果要在运行时动态改变内容,还得靠代码。

1.2 代码控制:动态创建 QLabel

在构造函数里写代码创建 QLabel,是更灵活的方式。下面是一个最小示例:

cpp 复制代码
#include <QLabel>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    QLabel *label = new QLabel("动态创建的标签", this);
    label->setGeometry(50, 50, 200, 30);
    label->setStyleSheet("color: blue; font-size: 16px;");
}

注意 new QLabel(..., this) 中的 this,它把 label 挂到了当前窗口的对象树下,后面我们会详细讲。运行后,窗口左上角会出现一行蓝色文字。

1.3 QString:Qt 的字符串王牌

QLabel 显示的内容,绝大多数时候来自 QString。它是 Qt 的 Unicode 字符串类,支持隐式共享,效率很高。常用操作如下:

cpp 复制代码
QString name = "Qt";
QString version = "6.5";

// 拼接
QString full = name + " " + version;          // "Qt 6.5"

// 格式化
QString msg = QString("版本号:%1,发布于 %2").arg(version).arg("2023年");

// 数字转字符串
int count = 42;
QString num = QString::number(count);         // "42"

label->setText(msg);

这里 %1、%2 是占位符,arg() 按顺序替换。相比 C 语言的 sprintf,这种方式类型安全,也更易读。

二、对象树机制

对象树是 Qt 内存管理的核心。理解它,你才能写出不泄漏、不崩溃的 Qt 程序。这一节我们从概念讲起,再演示怎么挂、不挂会怎样。

2.1 什么是对象树

Qt 的 QObject 类维护了一个父子关系链表。当你创建一个 QObject 子类对象并指定 parent 时,这个对象就会被加入父对象的 children 列表。父对象析构时,会依次 delete 所有子对象。这棵树就是对象树。

QWidget 继承自 QObject,所以所有控件天然支持对象树。窗口是根,控件是枝叶,层层嵌套,构成整个界面。

2.2 怎么挂到对象树上

挂载方式很简单:在构造函数里传入 parent 指针即可。

cpp 复制代码
// 方式一:构造时指定父对象
QLabel *label1 = new QLabel("挂载", this);

// 方式二:先创建,后 setParent
QLabel *label2 = new QLabel("后挂载");
label2->setParent(this);

两种方式效果等价。推荐方式一,代码更紧凑,也避免忘记挂载。

2.3 不挂会怎样

如果不指定 parent,对象就成了孤儿。它不会随窗口销毁,需要你手动 delete。一旦忘记,就是内存泄漏。更危险的是,如果窗口已经销毁,你还去操作这个孤儿控件,程序会直接崩溃。

cpp 复制代码
// 危险示例:不挂载
QLabel *leak = new QLabel("没人管我");
// 窗口关闭后,leak 依然存在,但没人 delete 它

在大型项目中,手动管理每个控件的内存极易出错。对象树机制把这件事自动化了:窗口关闭,整棵树一起销毁,干净利落。

三、继承扩展 MyLabel

光会用 QLabel 还不够,实际项目中我们经常需要定制控件。继承是 Qt 扩展控件的标准方式。这一节我们写一个完整的 MyLabel demo,并重写析构函数,亲眼观察对象销毁的时机。

3.1 定义 MyLabel 类

新建一个头文件 mylabel.h:

cpp 复制代码
#ifndef MYLABEL_H
#define MYLABEL_H

#include <QLabel>

class MyLabel : public QLabel
{
    Q_OBJECT

public:
    explicit MyLabel(const QString &text, QWidget *parent = nullptr);
    ~MyLabel() override;
};

#endif // MYLABEL_H

注意 Q_OBJECT 宏,它启用 Qt 的元对象系统,是信号槽和属性系统的前提。析构函数我们声明为 override,稍后实现。

3.2 实现析构函数

在 mylabel.cpp 中实现:

cpp 复制代码
#include "mylabel.h"
#include <QDebug>

MyLabel::MyLabel(const QString &text, QWidget *parent)
    : QLabel(text, parent)
{
    qDebug() << "MyLabel 构造:" << text;
}

MyLabel::~MyLabel()
{
    qDebug() << "MyLabel 析构:" << this->text();
}

构造函数里打印创建信息,析构函数里打印销毁信息。这样我们就能在控制台看到对象的完整生命周期。

3.3 在主窗口中使用

cpp 复制代码
#include "mylabel.h"

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    // 挂到对象树上
    MyLabel *label = new MyLabel("我是自定义标签", this);
    label->setGeometry(50, 50, 200, 30);
}

运行程序,控制台会输出「MyLabel 构造:我是自定义标签」。当你关闭窗口时,会看到「MyLabel 析构:我是自定义标签」。这说明对象树在窗口销毁时自动调用了析构函数,内存被正确释放。

3.4 观察销毁时机

为了更直观,我们再加一个按钮,点击后手动删除标签:

cpp 复制代码
QPushButton *btn = new QPushButton("删除标签", this);
btn->setGeometry(50, 100, 100, 30);

connect(btn, &QPushButton::clicked, this, [=]() {
    if (label) {
        delete label;
        label = nullptr;
    }
});

点击按钮,控制台立刻输出析构信息。注意:手动 delete 后,对象树会自动把该节点从 children 列表中移除,不会重复销毁。这就是 Qt 对象树的安全之处。

四、5 个坑

实战中,QLabel 和对象树有不少容易踩的坑。下面这 5 个是高密度避坑指南,建议收藏。

4.1 坑一:new 了不挂载也不 delete

这是最常见的泄漏来源。new 出来的控件不指定 parent,又忘记手动 delete,内存就永远收不回来。解决办法:要么构造时传 this,要么用智能指针。

4.2 坑二:重复 delete 同一对象

手动 delete 一个已经挂在对象树上的控件,之后窗口销毁时对象树会再次 delete 它,导致双重释放崩溃。正确做法:delete 后立即置空指针,或者干脆不手动 delete,交给对象树。

4.3 坑三:在析构函数里访问已销毁的子对象

父对象析构时,先析构子对象。如果你在父对象析构函数里访问子对象,此时子对象可能已经销毁,访问就是野指针。避免在析构函数里做复杂的子对象操作。

4.4 坑四:QString 与 const char* 混用

QString 的 + 运算符和 const char* 混用时,容易产生编码问题。中文尤其明显。建议统一用 QString,或者用 QString::fromUtf8() 显式转换。

cpp 复制代码
// 不推荐
QString s = "中文" + QString("拼接");

// 推荐
QString s = QStringLiteral("中文") + QStringLiteral("拼接");

4.5 坑五:setText 传入 nullptr

QLabel::setText 如果传入 nullptr,在某些版本会崩溃。安全做法是传入空字符串 QString() 或 "",不要传 nullptr。

五、代码汇总

下面把本文所有代码汇总成一个可直接运行的完整项目。新建 Qt Widgets Application,替换 mainwindow.h、mainwindow.cpp 和 mylabel.h、mylabel.cpp 即可。

5.1 mylabel.h

cpp 复制代码
#ifndef MYLABEL_H
#define MYLABEL_H

#include <QLabel>

class MyLabel : public QLabel
{
    Q_OBJECT

public:
    explicit MyLabel(const QString &text, QWidget *parent = nullptr);
    ~MyLabel() override;
};

#endif // MYLABEL_H

5.2 mylabel.cpp

cpp 复制代码
#include "mylabel.h"
#include <QDebug>

MyLabel::MyLabel(const QString &text, QWidget *parent)
    : QLabel(text, parent)
{
    qDebug() << "MyLabel 构造:" << text;
}

MyLabel::~MyLabel()
{
    qDebug() << "MyLabel 析构:" << this->text();
}

5.3 mainwindow.cpp

cpp 复制代码
#include "mainwindow.h"
#include "mylabel.h"
#include <QPushButton>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    MyLabel *label = new MyLabel("我是自定义标签", this);
    label->setGeometry(50, 50, 200, 30);

    QPushButton *btn = new QPushButton("删除标签", this);
    btn->setGeometry(50, 100, 100, 30);

    connect(btn, &QPushButton::clicked, this, [=]() {
        if (label) {
            delete label;
            label = nullptr;
        }
    });
}

把以上文件放入项目,编译运行。你会看到窗口上有一个自定义标签和一个按钮。点击按钮,控制台输出析构信息;关闭窗口,再次看到析构信息。整个生命周期一目了然。

六、下篇预告

本文我们掌握了 QLabel 的用法和对象树机制,但 Qt 真正的威力在于信号槽。下一篇,我们将深入讲解信号槽的完整机制:如何自定义信号、如何连接、如何传递参数,以及 Lambda 表达式的进阶用法。我们会用 QLabel 的点击事件作为切入点,带你亲手实现一个可交互的自定义控件。

如果你在练习中遇到任何问题,欢迎在评论区留言。我们下篇见。

相关推荐
hetao17338371 小时前
联盟场-提高组-R7 & ZYZSC-S-Round 11
c++·算法
程序喵大人1 小时前
【C++入门】编译链接模型 - 07 链接器怎样把符号对上号
开发语言·c++·编译器·编译链接·链接器
无敌贵点大王2 小时前
嵌入式软件面试经(自用)
c语言·stm32·单片机·学习·mongodb·面试·职场和发展
局部有解2 小时前
Hot100 - 三数之和
数据结构·c++·算法
Xudde.2 小时前
PwnLab: init 靶机渗透测试(Vulnhub Writeup)
笔记·学习·安全·web安全
我命由我123452 小时前
Function Calling 模型
人工智能·python·学习·ai·学习方法·ai编程·ai写作
longlongzihan2 小时前
C++ 常用数据结构速查:vector、stack、queue、list、树、图、哈希表等声明与核心操作
数据结构·c++·list
别动我齐刘海2 小时前
实时控制学习2
linux·c++·人工智能·学习·算法
海海不掉头发3 小时前
软件开发方法第七周笔记_CSDN博客
人工智能·笔记·学习·软件工程