初识Qt · 实现hello world的N种细节和对象树

发布于:2025-03-04 ⋅ 阅读:(6) ⋅ 点赞:(0)

目录

前言:

No.1->label

图形化的方式:

纯代码的方式:

内存泄漏的问题

乱码问题解释


前言:

继上文我们了解了QT的环境,历史的基本知识,以及了解了如何创建一个项目,项目的内容都包括什么,本文我们学习的是如何在GUI界面上打印Hello world,重要的不是hello world本身,而是在hello world背后牵扯到的N个知识点。

那么废话不多说,直接进入主题吧!


No.1->label

图形化的方式:

第一种打印hello world的方式我们打算使用控件label来实现。

我们还是快速的创建了一个项目。

使用label我们可以使用两种方式,第一种是ui界面直接使用图形化的方式创建,第二种是纯代码的方式进行创建。

首先,我们使用图形化的方式进行创建。直接点击form文件,在里面Display widgets找到label。

直接将它拖拽到ui界面即可:

那么运行:

也是成功运行了。

可是,如果只是介绍这点东西,那这QT简直就没啥可以学的。我们不妨看看其他文件出现了什么变化:

在以xml格式形成的form文件中,出现了以上的变化,什么string什么Hello world,什么class Qlabel什么name = label的。

这是因为qmake,当我们往ui界面拖拽了些许控件的时候,qmake在编译的时候就会通过xml文件生成一些C++代码,生成C++的代码我们想看就应该在该ui文件生成的头文件里面看:

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(310, 220, 191, 81));

        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

除了最上面的原本ui就有的代码,下面的方法中的label->setText就是qmake生成的代码。

这是我们使用图形化方式打印Hello world发生的变化。

纯代码的方式:

对于纯代码的方式,我们就需要用到C++的知识了。label这个控件,本质上是一个类,那么我们要使用这个类,大多数时候得包它的头文件的,如果有的时候我们没有包括某个头文件,但是能正常使用某个类的话,一般都是因为该类的头文件被间接包含了,对于label,或者说对于QT中的类的头文件都是对应的同名的头文件:

可是当我们引用到了该头文件的时候,会发现为什么会有两个头文件?

好像我们在C++和C语言混用的时候也出现了?

这是因为上古时期,QT使用的都是.h的文件,但是呢,在98年C++98成立之后,规定包含头文件都是以cstdio这种形式包含,所以我们之后包含头文件的话都是应该使用QLabel这种形式了。

那么new了之后,label的构造函数我们应该如何构建?其实,在上文,我们介绍过了对象树的概念,至于为什么需要对象树,我们下文介绍。在该label的初始化中,我们将参数给定为this,就是说label的父对象是widget,先构建好。

初始化之后,我们设置文本的方法在前文的ui文件生成的头文件也已经告诉我们了,使用的是setText方法:

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    QLabel* label = new QLabel(this);
    label->setText("Hello world");
}

此时我们运行试试:

可以发现,在ui界面的左上角同样出现了一个Hello world,默认是出现在QT坐标系的左上角,我们也没有设置对应的坐标。这是纯代码的方式。

在纯代码的方式,我们涉及到了新的知识点是:对象树,坐标系

好了,以为到这里就结束了吗?并没有,如果我们细看setText方法里面的内容提示:

发现它的参数其实是QString,而不是我们熟知的string,这是为什么呢?

其实在当年,表示一个字符串的话,C语言肯定是不好用的,因为字符串都是以\0结尾的,不巧的是,当时的C++也并不是很好用,所以QT就自己造了一套轮子,比如QString,QVector等,但是C++后面的容器什么的也是越做也好了,这就尴尬了, QT不能又给自己的轮子删除了吧?所以在后面的时候,就让两种容器共存了,并且QT也提供了将C语言风格的字符串隐式转换为QString的构造函数。所以我们可以写成label->setText(QString("Hello world")),但是不用,直接写就行。这其实是一个历史问题。

而实际上呢,对于QString来说,在字符编码方面处理的比std::string要好一点,但是现在咱们对于编码方面的处理了解还不是很好,所以咱们也就听听~~


内存泄漏的问题

细心的人会发现我们创建一个label的时候,实例化该对象的时候都是使用的new,为什么不是在栈上创建呢?

当某个控件,或者说某个对象,在失去作用的时候,就代表该UI界面出现bug了,失效了,那么什么是失去作用呢?比如该对象出了自己的栈帧空间,就像这样:

可以发现我们创建的labelone并没有在上面打印出来我们想要的Hello qt,这实际上就是因为该对象随着构造函数的销毁而销毁了。

所以我们都是十分推荐使用new,我们甚至不用担心内存泄漏的问题,因为QT有自己的机制可以让new出来的对象自动析构了。

是否记得对象树?

当我们new了一个对象的时候,该对象会在对象树上指定一个父类对象,当父类对象析构的时候,会带着它析构,其实就像Linux中的目录树状结构:

 我们光这样说了,不妨借助打印看看:

文件中选择添加新项目,选择C++:

class name即我们的类名,同样需要选择父类,但是呢,在这里base class的选择里面并没有我们希望的父类,所以我们不妨自己手写一个QLabel:

就生成了对应的.cpp文件和对应的.h文件:

#ifndef MYLABEL_H
#define MYLABEL_H
#include <QLabel>

class Mylabel : public QLabel
{
public:
    Mylabel(QWidget* parent);

};

#endif // MYLABEL_H

在对应的.h文件里面,我们只需要将构造函数的参数变成QWidget*即可。

这里有个小技巧:alt + enter可以快速切换声明和定义,F4可以快速切换cpp文件和h文件

在对应的.cpp文件里面:

#include "mylabel.h"

Mylabel::Mylabel(QWidget* parent) 
    : QLabel(parent)
{

}

调用了它对应的父类的构造,才能让我们这个自定义的类挂接到对象上,就像ABC,B先到了树上,带着C才能到。

那么我们需要打印一个信息,即自己被析构了,我们打算在析构函数里面写:


Mylabel::~Mylabel()
{
    std::cout << "Mylabel 被销毁!!" << std::endl;    
}

好了,开始实验:

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    Mylabel* mylabel = new Mylabel(this);
}

Widget::~Widget()
{
    delete ui;
}

在构造函数我们new了一个对象,运行程序并关闭程序:

在下面的应用程序输出确实打印了点什么东西,这也证明了QT确实存在自己的内存释放机制。

接下来,我们应该解决的是乱码问题。


乱码问题解释

解释这个乱码问题之前,不妨回答一个问题:一个汉字是由多少个字节构成的?

这个问题百分之90的人都会回答错,只要回答了一个具体的数字,基本就是错的,因为这个问题的答案取决于当前用的是什么字符集。

也就是我们常说的编码,对于不同的编码,汉字对应的字节有就不一样,比如GBK,我们在VS上试验打印出来是两个字节,这是因为Windows默认的是GBK编码,但是如果我们使用的是Linux环境,默认的是utf8编码,此时打印出来就是3个字节了。

所以,取决于对应的编码。

好了,现在的问题是,QT界面使用的是什么编码?

当我们点击任意的文件,选择使用记事本打开,并且点击另存为,此时右下角出现的就是编码方式,utf8代表的就是utf8编码方式,ASCII代表的就是GBK方式。

问题来了,我们是修改对应的字符串的编码方式还是QT的编码方式呢?似乎都不是很妥当。

所以在QT里面的QString很好的处理了编码方式,不仅如此,QT中也提供了专门用来打印日志的工具,它是一个宏,也很好的处理了编码,这个宏也是可以一键关闭的,使用如下:

#include "mylabel.h"
#include <QDebug>
Mylabel::Mylabel(QWidget* parent)
    : QLabel(parent)
{

}

Mylabel::~Mylabel()
{
   qDebug() << "Mylabel 被销毁!!";
}

就非常完美了。

以上是乱码问题的解释。

一个小结:

小结:
1.认识 QLabel 类,能够在界面上显示字符串.通过 setText 来设置的. 参数 QString(Qt 中把 C++ 里的很多容器类, 进行了重新封装. 历史原因


2.内存泄露/文件资源泄露


3. 对象树, Qt 中通过对象树,来统一的释放界面的控件对象.
Qt 还是推荐使用 new 的方式在堆上创建对象,通过对象树,统一释放对象创建对象的时候,在构造函数中,指定父对象(此时才会挂到对象树上)
如果你的对象没有挂到对象树上,就必须要记得手动释放!!


4. 通过继承自 Qt 内置的类, 就可以达到对现有控件进行功能扩展效果.
Qt 内置的 QLabel, 没法看到销毁过程的,为了看清楚,就创建类 MyLabel, 继承自 QLabel
重写析构函数.
在析构函数中,加上日志,直观的观察到对象释放的过程了,

5.乱码问题 和 字符集~ MySQL(很多地方都涉及到)

6.如何在 Qt 中打印日志,作为调试信息
(具体这个宏叫啥名字,太长了,俺也记不住)

使用 cout 固然可以, 但是并不是上策(字符编码处理的不好,也不方便统一进行关闭)

Qt 中推荐使用 qDebug() 完成日志的打印.

7.为啥要打印日志调试呢??
调试器很多时候是有局限性的,是无法使用的.
假设当前 bug 是一个概率性的 bug, 出现的概率是 1% 甚至更小要想调试,无法使用调试器了.
无论是哪种方式,本质上都是观察程序执行的中间过程和中间结果~
使用日志, 就可以很好的解决这种问题~~


感谢阅读!