小编个人主页详情<---请点击
小编个人gitee代码仓库<---请点击
Qt系列专栏<---请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
目录
-
- 前言
- 一、HelloWorld的实现
-
- [通过图形化的方式创建hello world](#通过图形化的方式创建hello world)
- 使用代码创建HelloWorld
- 二、对象树
- 总结
前言
【Qt】Qt概述(二)环境搭建,Qt SDK,Qt Creator创建项目,项目代码的解释------书接上文 详情请点击<------
本文由小编为大家介绍------【Qt】Qt概述(三)Qt初识,HelloWorld的创建,对象树
一、HelloWorld的实现
- hello world对于我们学习任何一门语言,技术都是起点,那么同样的道理,Qt技术可以让我们开发GUI图形化程序界面,那么我们学习Qt也想要在界面上显示hello world该如何做呢?实际上我们可以有两种方式,如下
(1)通过图形化的方式,在界面上创建出一个控件,显示hello world
(2)通过纯代码的方式,通过编写代码,在界面上创建出一个控件,显示hello world - 下面小编就来逐个讲解一下
通过图形化的方式创建hello world
- 那么接下来我们打开Qt Creator,关于如何创建文件 详情请点击<------,小编在上一篇文章中已经进行讲解了,这里就不再赘述了,所以此时我们就创建了一个项目名为HelloWorld,基类为QWidget的类Widget

- 那么我们此时位于左侧的编辑区域,然后我们双击Forms目录下的widget.ui

- 此时我们就切换到了左侧的设计区域,并且在左边Qt的内置控件下滑,找到Display Widgets翻译过来就是显示控件,即用于进行显示的控件,那么其中的第一个Label
- 那么关于图形化方式创建hello world的方式也十分简单,此时我们使用鼠标左键长按Label,然后拖拽到中间的程序窗口

- 观察上图,此时小编已经将Label拖拽到中间的程序窗口了

- 接下来可以拖拽六个边角进行适当的调节标签Label所在的位置以及大小,如上小编已经调整到了比较适中的位置

- 那么接下来双击标签位置就可以对文件进行编辑,文本原内容是TextLabel,此时我们修改成我们想要的hello world即可

- 此时文本内容修改完成,双击程序窗口内的除了Label以外的区域,让修改完成生效

- 所以此时我们点击左下角的▶按钮,就可以编译运行代码,此时显示的程序窗口上就有hello world字样了,小编,就这么简单?是的,通过图形化的方式创建hello world就这么简单

- 同样的我们还可以看到上上图的右上角,小编已经截图下来了在上图,即那么在Qt Designer的右上角,通过树形结构,显示出了当前界面上都有哪些控件,有一个Widget类继承自QWidget类,还有一个label对象的类型是QLabel类

- 但是我们关注的不是这里,那么我们返回左侧的编辑区域,然后点击widget.ui,查看xml,发现原有的内容已经发生了改变,其中已经有了class = Qlabel,name = label,表示创建了Qlabel类型的类,对应的类对象名称为label,并且红色框的倒数第二行也包含有hello world字样,并且中间通过数字坐标x,y描述了标签要显示的hello world所在的位置
- 所以,刚才向界面上拖拽了一个QLabel控件,此时,ui文件的xml就会多出一段上图红色框内的代码,进一步的,qmake就会在编译项目的时候,基于这个内容生成一段C++代码,进而通过C++代码就可以构建出界面内容了,上述这一系列操作都是自动完成的,所以我们想要看一看这个C++代码该如何做呢?如下

- 那么接下来我们来看一下右击工程名HelloWorld.pro,点击在Explorer中显示,此时就会跳转到文件资源管理器

- 此时我们要返回上级目录,所以此时我们点击上方路径的qt即可进行跳转

- 接下来我们点击build-HelloWorld这个目录进入

- 此时就会进入如上目录,那么最下面,有一个ui_widget.h这个.h文件,那么我们鼠标点击长按拖拽到Qt Creator中即可查看,所以此时我们就会看到如下代码
cpp
/********************************************************************************
** Form generated from reading UI file 'widget.ui'
**
** Created by: Qt User Interface Compiler version 5.14.2
**
** WARNING! All changes made in this file will be lost when recompiling UI file!
********************************************************************************/
#ifndef UI_WIDGET_H
#define UI_WIDGET_H
#include <QtCore/QVariant>
#include <QtWidgets/QApplication>
#include <QtWidgets/QLabel>
#include <QtWidgets/QWidget>
QT_BEGIN_NAMESPACE
class Ui_Widget
{
public:
QLabel *label;
void setupUi(QWidget *Widget)
{
if (Widget->objectName().isEmpty())
Widget->setObjectName(QString::fromUtf8("Widget"));
Widget->resize(800, 600);
label = new QLabel(Widget);
label->setObjectName(QString::fromUtf8("label"));
label->setGeometry(QRect(300, 250, 171, 51));
retranslateUi(Widget);
QMetaObject::connectSlotsByName(Widget);
} // setupUi
void retranslateUi(QWidget *Widget)
{
Widget->setWindowTitle(QCoreApplication::translate("Widget", "Widget", nullptr));
label->setText(QCoreApplication::translate("Widget", "hello world", nullptr));
} // retranslateUi
};
namespace Ui {
class Widget: public Ui_Widget {};
} // namespace Ui
QT_END_NAMESPACE
#endif // UI_WIDGET_H

- 那么我们重点关注setupUi代码,看其中就有对QLabel标签控件类对象label的创建,并且这个申请是在堆上进行申请的,并且其中就包含了对参数坐标的设置,所以这也就意味着我们接下来如果想要使用纯代码的方式,让界面上显示hello world也需要这样的方式,即使用代码创建QLabel标签控件类对象,然后进行设置,如下
使用代码创建HelloWorld
- 那么见识完使用图形化方式,在界面上创建一个控件,在界面上显示hello world,那么接下来我们来看一下如何通过纯代码的方式,通过代码,在界面上创建一个控件,在界面上显示hello world

- 那么同样的道理,右上角点击文件,然后选择关闭所有项目和编辑器就可以将当前项目关闭,然后接下来我们创建一个项目名为HelloWorld_2,继承基类选择QWidget,而派生类的名称为widget的项目

- 一般通过代码来构造界面的时候,通常会把构造界面的代码放到Widget/MainWindow的构造函数中
- 那么在之前的图形化创建HelloWorld的讲解中,我们已经知道了,我们通过代码的方式创建HelloWorld的方式就要先创建控件QLabel的类对象label

- 那么我们来看,此时的Qt不认识QLabel,没关系,在Qt中,每个内置的类都有一个对应的同名头文件,所以此时我们包含以下QLabel的头文件即可

- 那么这里有细节,小编,小编,我们是看到了同名头文件QLabel,可是下面为什么还有一个qlabel.h呢?其实这是有历史原因的
- 在上古时期,即1991-1998年之间,Qt使用的是qlabel.h这种风格的头文件,1998年之后,C++标准委员会成立了,推出了C++98,即C++98标准,规定,统一使用#include <cstdio>去替代#include <stdio.h>,即在C++98标准中包含头文件的风格不带.h,而Qt作为C++的大股东,Qt也响应号召,支持C++,所以Qt有了不带.h风格,Q开头的头文件,那么为什么不把之前的.h头文件删除呢?
- 首先是C++98不强制,也就意味着原有的.h风格的头文件还是可以包的,还有一句话,向前兼容,因为有的使用Qt实现的GUI图形化程序的头文件可能就是包的.h风格的,一旦Qt将.h的删除了,那么之前的代码就无法运行,就需要重新改代码,然后部署上线,还有可能会导致Bug,所以软件设计上只能是向前兼容,那么我们自己包头文件小编建议使用没有.h,以Q开头的这种头文件进行包含
- 那么接下来我们包含完头文件,接下来我们就该创建对象了,那么我们究竟是在堆上创建还是在栈上创建呢?不知道,那怎么办,照猫画虎呗,小编瞅着前面图形化方式创建hello world中Qt自动生成的代码中是这样做的,所以此时在堆上创建QLabel对象了,至于为什么要在堆上创建呢?小编在后面的对象树环节进行阐述

- 所以此时我们new一个QLabel即可,由于是指针,所以我们使用QLabel*的指针对象label接收即可,此时问题来了,如上new一个QLabel要进行传参一个父对象的指针,那么在这里一旦进行传参,代表此时使用对象树机制,当然由于有缺省参数nullptr也可以不传参,不进行传参表示我们不使用对象树机制,而我们要使用对象树机制,所以我们就要传参,那么该如何传参呢?
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QLabel>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QLabel* label = new QLabel(this);
}
Widget::~Widget()
{
delete ui;
}
- 这里我们传入this指针,this指针指向的是Widget对象w至于原因解释,小编也留到对象树中进行解释,接下来我们就要设置文本了

- QLabel*类型的label指向的是一个QLabel控件,那么如果我们想要设置控件中,要显示的文本内容,那么就要调用setText方法,那么我们本能的想要传参一个字符串,但是我们在这里定睛一看参数的类型是QString,这是个什么鬼,我之前学习C++怎么没见过这个类型呢?那么这就说来话长了
- Qt诞生于1991年,那个时候C++还没有成立标准委员会,同样的也没有"标准库",当时要表示一个字符串,可以使用C风格的以'\0'结尾的字符串,也可以使用C++的string,注意C++的string的产生是早于STL,但是对于Qt来讲,无论是C风格的字符串还是C++的string都不太好用
- 于是Qt为了让自己的开发变得更流畅,于是自己发明了一套轮子,搞了一套基础类,来支持Qt的开发,例如QString字符串,QVector动态数组,QList链表,QMap字典等,几年后C++成立了标准委员会搞出来了STL
- 但是软件都是向前兼容的,已经引入的Qt自己包装好的这些容器是不可能删除了,所以Qt的这些以Q风格开头的容器只能和标准库的容器共存了,我们在已经有了C++基础之后,已经学习了C++的标准库之后,上手这些Qt的容器是不难的,都是类似的套路
- 因此在我们以后的Qt代码开发中,如果需要使用容器,可以使用C++标准库的,也可以使用Qt自己搞的这一套,但是毕竟是入乡随俗,并且在Qt的函数调用中,但凡涉及到的接口,都是使用的Qt自己的这一套,所以小编建议我们也是用Qt的这一套
- 因此在我们后续的学习中,我们会经常见到QString这一类的容器,而很少见到std::string标准库的容器,当然QString也支持了对std::string的转换,同样的QString也可以接受C风格的字符串进行隐式类型转换为QString类型的对象进行传参,但是相比较于std::string,QString内部是对字符编码进行处理了的,可以避免很多场景下的乱码情况
- 对于QString的头文件#include <QString>的头文件已经被很多Qt内置的类给隐式包含了,所以对于QString我们大多数场景下都是可以直接拿来用,对于QString的头文件很多场景下并不需要显示的去包含,所以这里我们使用C风格的字符串"hello world"进行传参即可
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QLabel>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QLabel* label = new QLabel(this);
label->setText("hello world");
}
Widget::~Widget()
{
delete ui;
}
- 所以接下来,我们点击左下角的▶就可以直接运行代码了
运行结果如下
- 此时在窗口的左上角就打印出了hello world,并且我们还可以看出默认如果我们不调用任何函数进行坐标的设置,那么打印出来的文本内容默认是在窗口的左上角,如果想要放到其它的位置,我们只需要使用对应的函数进行调整即可,那么至于使用什么函数以及如何进行调整小编会在后面的文章进行讲解
- 所以此时我们就实现了使用纯代码的方式,在界面中创建一个QLabel控件对象,然后在界面上显示出了hello world
二、对象树
原理讲解
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QLabel>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
QLabel* label = new QLabel(this);
label->setText("hello world");
}
Widget::~Widget()
{
delete ui;
}
- 所以呢?如果你是学习C++方向的,那么在小编使用new在堆上申请空间,那么你可能会下意识的觉得,诶,小编,你这样写不会导致内存泄露吗?诸如new申请的空间和文件描述符泄露都属于内存泄露的范畴,所以你还没有delete释放空间呢?
- 是的,小编没有写delete释放空间,所以这样不就内存泄露了吗?实则不然,不仅不会导致内存泄露,还有有人帮我们delete释放空间,这其中的缘由就是引入了对象树的机制,所以我们该如何进行理解呢?如下

- 所以如上图左侧就是程序窗口,程序窗口有很多不同的控件,引入对象树,通过这个树形结构,就把界面上要显示的控件对象组织起来了,最主要的目的就是可以在合适的时机,把这些对象进行统一释放,所以呢?我们该如何理解这里的合适的时机呢?

- 例如,如上是网易云音乐桌面版的主界面,那么网易云音乐背后也是使用Qt技术进行开发的,我们也可以把网易云音乐桌面版的主界面理解为一个程序窗口,这一个程序窗口上包含有很多的控件,例如播放按钮,上一首,下一首等控件,那么是否在程序窗口运行的时候,这些控件要消失呢?
- 不,一定不能,我使用网易云音乐就是为了播放音乐,突然播放控件▶消失了,那我用户还要不要使用你网易云音乐了,是不是,所以这肯定不行,此时我们可以得出位于程序窗口内的控件的生命周期都要随程序窗口,只有用户想要点击右上角的✖️关闭程序窗口的时候,这些控件才可以被释放

- 所以对于对象树上的这些对象,我们期望都是要进行统一销毁释放,如果某一个控件提前销毁,就会导致对应的控件在界面上不存在,所以我们要使用对象树将程序窗口上的所有控件对象组织起来,当程序窗口被关闭的时候,以树形结构进行逐个遍历delete释放即可,所以上面的合适的时机就是指当程序窗口被关闭/销毁的时候
- 并且对于树形结构来讲,一个节点可以有很多个子节点,一个节点只能有一个父节点,所以通过传入父节点对象的方式,我们才可以知道要将这个节点挂接到哪一个父节点的下面,即才可以知道将节点挂接到对象树的哪个位置
- QLabel* label = new QLabel(this),那么我们来看前面这个语句,此处通过new的方式创建对象,传入this指针就是为了告诉Qt要这个QLabel对象要挂接到哪个父节点的下面,挂接好后,此时这个对象的生命周期交给Qt的对象树来进行统一管理,即这个控件对象的生命周期就随程序窗口的生命周期了,当程序窗口被关闭/销毁的时候,控件才会被delete释放
- 所以此时小编可以回答一下了,是否我们没有写delete会导致内存泄露呢?不会,由于引入了对象树机制,Qt的控件对象会在合适的时机,即程序窗口关闭/销毁的时候由对象树机制帮我们自动进行delete释放,所以不会导致内存泄露
- 那么其实在前端网页(网页开发)上也涉及到对应的对象树机制(DOM),本质上也是一个树形结构(N叉树),通过树形结构把网页界面上的各种元素组织起来,同样的,Qt也是类似,也搞了一个对象树,通过这个N叉树把程序窗口界面上的各种元素组织起来,进行统一的管理释放
- 那么此时我们试想一下,如果这个控件对象是在栈上创建的,那么此时一旦出了作用域,那么此时就要调用析构函数释放控件对象,即此时会出现"提前释放"的问题,所以程序窗口上也就不会出现对应的控件了 ,所以此时小编修改一下代码,将QLabel对象的创建由new从堆上进行申请修改为在栈上定义,如下
cpp
#include "widget.h"
#include "ui_widget.h"
#include <QLabel>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// QLabel* label = new QLabel(this);
// label->setText("hello world");
QLabel label(this);
label.setText("hello world");
}
Widget::~Widget()
{
delete ui;
}
运行结果如下
- 所以此时,当小编把代码改成从栈上创建,此时就可以看到运行起来的程序,无法显示出控件,无法显示出hello world,由于的QLabel类对象label是栈对象,此时随着Widget构造函数的结束,那么出了构造函数作用域label自动调用析构函数释放了,所以此时程序界面上也不会有QLabel对象,更不会显示出hello world
- 所以在实际编写的情况下,我们更推荐使用new在堆上申请,然后通过传入父对象挂接到对象树中,让控件的生命周期随程序窗口,让对象树帮我们管理控件对象的生命周期
代码验证
- 小编,小编,你光说挂接到对象树的控件对象释放了,就是释放了吗?万一你骗我怎么办,所以我想见一见,至少给我打印个日志看一看,所以我们该如何做呢?即此时我们想要让控件对象的析构函数可以打印日志,当程序窗口结束,对象树自动调用delete释放控件对象的时候,会调用控件对象的析构函数,而这个控件对象的析构函数中包含主我们的打印日志,我们期望看到这个打印日志,所以呢?
- 此时Qt内置的QLabel类就无法满足我们的需求,那么很简单,我们创建一个MyLabel类然后让它继承Qt内置的QLabel不就好了,此时我们可以让MyLabel的析构函数进行打印日志,并且同样的也可以MyLabel也继承了QLabel的方法setText,所以MyLabel可以调用setText让界面上显示hello world,一举两得,所以此时我们就要创建一个MyLabel类了,那么在Qt中如何创建一个类呢?如下

- 同样的是左上角点击文件,然后点击第一个新建文件或项目

- 此时我们要点击的就是右下角的文件和类中的C++,由于我们要创建的是C++类MyLabel然后点击中间的C++ Class,接下来点击Choose进行下一步

- 那么此时我们来看,Class name我们填写MyLabel即可,然后第二行要继承的类名,而我们要继承的类名不在其中,所以我们在第三行手动打一下类的名称QLabel即可,接下来点击下一步

- 接下来我们继续点击完成即可,此时我们就完成了对继承自Qt内置的类QLabel的派生类MyLabel的创建


- 所以此时Qt就帮我们生成了MyLabel的.h文件和.cpp文件,那么从左侧我们可以看到确实Qt帮我们生成了MyLabel类的声明,但是问题来了,你都帮我声明了,并且MyLabel继承的还是你Qt内置的类QLabel,好人做到底,你顺便也帮我把QLabel的头文件也包含了呗,所以我们观察上图,Qt并没有帮我们将QLabel的头文件包含
- 所以Qt是帮我们生成了一些代码,但是没有完全生成,对应的头文件没有给我们包含,我们需要自己手动包含,同样的我们也看一下,对于析构函数也没有给咱生成,所以我们也要手动声明一下析构函数
cpp
#ifndef MYLABEL_H
#define MYLABEL_H
#include <QLabel>
class MyLabel : public QLabel
{
public:
MyLabel(QWidget* parent);
~MyLabel();
};
#endif // MYLABEL_H
- 接下来,完成头文件的包含和析构函数的声明之后,我们编写一个构造函数的参数,这里由于要实现对象树机制,所以这里我们需要让当前对象接收一个父节点,那么此时我们使用QWidget即可,因为当前MyLabel的控件对象是要放到Widget这个程序窗口的界面中的,而Widget是继承自QWidget,此时使用QWidget的指针接收Widget对象发生切片,不会存在任何问题,所以我们也就知道了该如何编写MyLabel的构造函数

- 此时小编还要讲解一个Qt Creator的使用小技巧,此时我们是位于MyLabel的.h头文件中,按住F4或者fn+F4就可以实现.h头文件到.cpp源文件的切换

- 所以使用fn+F4就完成了.h文件和.cpp文件的切换,所以此时我们完成MyLabel的构造函数的编写即可
cpp
#include "mylabel.h"
MyLabel::MyLabel(QWidget* parent)
: QLabel(parent)
{}
- 所以此时我们将MyLabel的构造函数的形参设置为QWidget* parent,用于接收Widget的指针,接下来我问你,我们的MyLabel类中是否实现了对象树机制呢?
- 没有,虽然我们自己没有实现对象树,但是别忘了我们自己的派生类MyLabel可是继承了Qt的内置类QLabel,而Qt的内置类QLabel中就引入了对象树机制,既然派生类MyLabel继承了Qt的内置类QLabel,所以意味着派生类也拥有基类的方法以及机制

- 所以通过继承的手段,我们的派生类MyLabel也拥有对象树的机制,所以那么我们回顾一下上面,对象树的机制可以使用,也可以不使用,那么此时要完成派生类MyLabel的构造函数之前,要先调用基类QLable的构造函数
cpp
#include "mylabel.h"
MyLabel::MyLabel(QWidget* parent)
: QLabel(parent)
{}
- 所以此时对象树机制不就来了,我们调用QLable的构造函数机制,将parent传入即可,所以一旦QLabel挂接到了对象树上,此时继承下来的基类QLabel是派生类MyLabel的一部分,那么自然的派生类MyLabel也挂接到了对象树中,所以很完美

- 那么接下来小编还要传授一个Qt Creator的小技巧,那么首先我们按fn+F4切换到.h文件,那么我们将光标放到析构函数上,然后按下alt+enter(也就是所谓的回车键)

- 那么选中第一个在mylabel.cpp中添加定义,那么继续按下enter回车

- 此时在就会自动切换到.cpp文件,并且自动添加对应函数的定义,这也是Qt Creator使用的小技巧,通过这些小技巧,可以在一定程度上减少我们的工作量,提升我们写代码的幸福感哈
cpp
#include "mylabel.h"
#include <iostream>
MyLabel::MyLabel(QWidget* parent)
: QLabel(parent)
{}
MyLabel::~MyLabel()
{
std::cout << "MyLabel delete" << std::endl;
}
- 所以此时我们在析构函数中使用cout打印日志信息即可,此时我们就完成了对MyLabel的实现
cpp
- 接下来,我们就可以进行对MyLabel的调用,那么如何调用呢?在Widget的构造函数中进行调用,传入this指针将MyLabel对象mylabel挂接到对象树上,然后接下来调用MyLabel继承自QLabel的setText方法进行Hello World的在界面上的显示即可
cpp
#include "widget.h"
#include "ui_widget.h"
#include "mylabel.h"
#include <QLabel>
Widget::Widget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::Widget)
{
ui->setupUi(this);
// QLabel* label = new QLabel(this);
// label->setText("hello world");
// QLabel label(this);
// label.setText("hello world");
MyLabel* mylabel = new MyLabel(this);
mylabel->setText("Hello World");
}
Widget::~Widget()
{
delete ui;
}
运行结果如下
- 诶,小编不对呀,我预想中的delete MyLabel呢?为什么没有呢?那么我们仔细思考一下,此时我们的MyLabel对象mylabel已经挂接到对象树上了,也就意味着MyLabel控件的生命周期已经随程序窗口了,所以此时程序窗口被关闭/销毁了吗?没有,并且一旦程序窗口被关闭/销毁了,那么程序输出的结果应该去哪找呢?此时我们点击下方序号3的应用程序输出即可进行观察,所以此时我们就点击程序窗口的✖️即可
运行结果如下
- 所以此时我们观察到了日志MyLabel delete,说明析构函数执行了,即此时虽然我们并没有手动调用delete,但是由于把MyLabel挂接到了对象树上,让MyLabel控件的生命周期随程序窗口,通过对象树机制管理MyLabel控件对象,当程序窗口界面关闭/销毁的时候,那么此时通过对象树机制,就会遍历对象树中所有的节点对象,进行逐个调用delete进行析构释放,进而MyLabel的析构函数就会被执行到,进而我们才可以看到打印的日志MyLabel delete
- 所以我们对于控件对象的创建都采用new的方式,然后传入父对象指针挂接到对象树中,控件对象的生命周期随程序窗口界面,当程序窗口界面被关闭/销毁的时候,Qt Creator会自动遍历对象树中所有的节点对象,进行逐个调用delete进行析构释放,所以有了对象树机制,我们不怕调用new在堆上申请空间创建控件对象,当控件对象使用完成的时候忘记调用delete进行析构进而造成的内存泄露的情况了,因为对象树机制会帮我们自动调用delete进行析构释放
总结
以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!




