【QT】编写第一个 QT 程序 & 对象树 & Qt 编程事项 & 内存泄露问题

目录

[1. 编写第一个 QT 程序](#1. 编写第一个 QT 程序)

[1.1 使用 标签 实现](#1.1 使用 标签 实现)

[1.2 纯代码形式实现](#1.2 纯代码形式实现)

[1.3 使用 按钮 实现](#1.3 使用 按钮 实现)

[1.3.1 图形化界面实现](#1.3.1 图形化界面实现)

[1.3.2 纯代码形式实现](#1.3.2 纯代码形式实现)

[1.4 使用 编辑框 实现](#1.4 使用 编辑框 实现)

[1.4.1 图形化界面实现](#1.4.1 图形化界面实现)

[1.4.2 纯代码形式实现](#1.4.2 纯代码形式实现)

[1.4.3 内存泄露](#1.4.3 内存泄露)

[2. 认识对象模型(对象树)](#2. 认识对象模型(对象树))

[2.1 什么是对象树](#2.1 什么是对象树)

[2.2 验证对象树](#2.2 验证对象树)

[3. 解决编码问题](#3. 解决编码问题)

[4. Qt 编程注意事项](#4. Qt 编程注意事项)

[4.1 Qt 中的命名事项](#4.1 Qt 中的命名事项)

[4.2 Qt Creator 中的快捷键](#4.2 Qt Creator 中的快捷键)

[4.3 使用帮助文档](#4.3 使用帮助文档)

[5. 小结](#5. 小结)


1. 编写第一个 QT 程序

1.1 使用 标签 实现

1.创建好一个项目后,我们可以点击 widget.ui 进入图形化界面设计,可以直接通过拖拽的方式进行添加

2.拖拽 "标签" 至 UI 设计界面中,并双击修改标签内容

3. 此时ui界面就会生成对应的 XML 格式代码,这个时候qmake就会根据这个XML代码生成对应的C++代码,我们也可以在同目录下找到这个C++代码

1.2 纯代码形式实现

我们点击 widget.cpp 里面,会有一个 widget 的构造函数和析构函数,我们一般使用代码进行编辑界面的时候,一般都是在 widget 的构造函数中实现,因为在 main 函数中调用了 widget 类之后就直接 show了,所以卸载构造函数中的时候,一旦执行到了 show 就一定可以显示出设计的界面

cpp 复制代码
#include "widget.h" // 创建生成时的文件
#include "ui_widget.h"
#include <QLabel>  // 包含标签的头文件
 
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this); // 将form file生成的界面和我们当前的widget进行关联起来
    // 创建对象的两种方法
    // QLabel label; // 在栈上创建
    QLabel* label = new QLabel(this); // 在堆上创建,推荐这种方法,还要传递 一个 this,给当前这个 lable 对象指定 父对象
 
    // 1. 设置标签内容
    label->setText((QString)("显式 Hello world"));
    label->setText("隐式 Hello World"); // QString 也提供了 C 风格字符串作为参数的构造函数来不显示构造 QString
    // 注意:由于QString 对应的头文件,已经被很多 Qt 内置的其他类给间接包含了.因此一般不需要显式包含 QString 头文件
    // 这里虽然有两次 setText,但是下面内容会覆盖上面内容
 
    // 2. 设置窗口大小
    setFixedSize(500, 400);
 
    // 3. 设置字体大小
    QFont font("楷体", 16);
    label->setFont(font);
 
    // 4. 设置标签内容显式位置
    label->move(200, 150);
 
    // 5. 设计标签字体颜色
    label->setStyleSheet("color:blue");
}
 
Widget::~Widget()
{
    delete ui;
}
 
cpp 复制代码
void setText(const QString &);

这里我们会发下使用字符串的时候并不是我们 C++ 使用的标准库中的 string,而是 Qt 自己包装好的字符串 QString 。这个其实也是历史原因,Qt 诞生于1991年,那个时候 C++ 还没有定标准,而 Qt 为了更好的开发就自己包装了一些容器。但是我们也还是可以使用 C++ 的标准库中的容器来使用

1.3 使用 按钮 实现

1.3.1 图形化界面实现

1. 双击:"widget.ui" 文件

2. 拖拽控件至 ui 界面窗口并修改内容

  • 虽然那里有好几个按钮,但是我们这里用 Push Button(普通按钮)

3. 构建并运行,效果如下所示

  • 这里的按钮的确可以点击,但是却没有任何反应,这个就设计到我们后面学的信号槽知识,后面会说的

  • QT 的信号槽机制:本质上就是给按钮的点击操作,关联上一个处理函数,当用户点击的时候,就会执行这个处理函数

这里我们的按钮没有任何功能,假如我们要实现一定的功能,那该怎么做呢?

打开 widget.ui 文件,查看设计的右下角,则有

如下代码:

widget.hpp

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

QT_BEGIN_NAMESPACE
namespace Ui {
class Widget;
}
QT_END_NAMESPACE

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    void handleClick();
    ~Widget();

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H

widget.cpp

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
 
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    // 按钮的点击操作 -- 信号槽
    // 在 Linux 网络编程那也有个connect 函数,那里用来给 TCP socket 建立连接的,写 TCP 客户端的时候,就需要先建立连接才能读写数据
 
    // ui->pushButton:谁发出的信号
    // &QPushButton::clicked:发出了啥信号,点击按钮的时候自动触发该信号
    // this: 谁来处理这个信号
    // Widget::handle:具体怎么处理
    connect(ui->pushButton, &QPushButton::clicked, this, &Widget::handleClick);  // 访问到 form file(ui 文件)中创建的控件
 
}
 
Widget::~Widget()
{
    delete ui;
}
 
void Widget::handleClick()
{
    if(ui->pushButton->text() == "Hello World"){
        ui->pushButton->setText("Hello IsLand");
    }else{
        ui->pushButton->setText("Hello World");
    }
}
 

返回上级目录查看 ui_widget.h 文件

因此我们也可以把 PushButton 改成其他的,如下:

再次查看 ui_widget.h 文件,如下:

结论:在 objectName 中,设置成什么值,生成的变量名就叫啥名字就可以根据这个名字来获取到对应的控件的变量了

1.3.2 纯代码形式实现

widget.h

cpp 复制代码
#ifndef WIDGET_H
#define WIDGET_H
 
#include <QWidget>
#include <QPushButton>
 
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
 
class Widget : public QWidget
{
    Q_OBJECT
 
public:
    Widget(QWidget *parent = nullptr);
    void handleClick();
    ~Widget();
 
private:
    Ui::Widget *ui;
 
    QPushButton* myButton;
};
#endif // WIDGET_H

widget.cpp

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
 
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);
    myButton = new QPushButton(this);
    myButton->setText("Hello World");
    connect(myButton, &QPushButton::clicked, this, &Widget::handleClick);  // 访问到 form file(ui 文件)中创建的控件
}
 
Widget::~Widget()
{
    delete ui;
}
 
void Widget::handleClick()
{
    if(myButton->text() == QString("Hello World")){
        myButton->setText("Hello IsLand");
    }else{
        myButton->setText("Hello World");
    }
}

实现效果如下图:

两个版本比较:

  • 图形化实现:此时按钮对象不需要咱们自己 new。new 对象的操作已经是被 Qt 自动生成了而且这个按钮对象,已经作为 ui 对象里的一个成员变量了,也无需作为 Widget 的成员

  • 纯代码实现:按钮对象是咱们自己 new 的,为了保证其他函数中能够访问到这个变量,就需要把按钮对象,设定为 Widget 类的成员变量

实际开发中,是通过代码的方式构造界面为主,还是通过图形化界面的方式构造界面为主??

  • 这两种都很主要,难分主次!!

  • 如果你当前程序界面,界面内容是比较固定的,此时就会以 图形化 的方式来构造界面

  • 但是如果你的程序界面,经常要动态变化,此时就会以 代码 的方式来构造界面

  • 反正这两种方式哪种方便用哪个,也可以配合来使用

1.4 使用 编辑框 实现

  • 单行编辑框: QLineEdit
  • 多行编辑框: QTextEdit
1.4.1 图形化界面实现
  • 当然输出的文本框,我们也可以在输出里面进行修改啥的,但是不会影响代码里面的文本数据
1.4.2 纯代码形式实现
1.4.3 内存泄露

在上面的代码实现中,我们使用 new创建了对象,在栈上开辟了一块空间之后,但是我们没有使用delete进行释放控件,这样不就会导致内存泄漏啊

  • 其实上述代码在 Qt 不会产生内存泄露, label 对象会在合适的时候被析构释放,之所以能够把对象释放掉,主要是因为把这个对象挂到了 对象树 上

  • 前端开发(网页开发)也涉及到 类似的 对象树 (DOM),本质上也是一个树形结构(N 又树),通过树形结构把界面上的各种元素组织起来

  • Qt 中也是类似,也是搞了一个对象树,也是 N 又树,把界面上的各种元素组织起来了

  • 用对象树把这些内容组织起来,最主要的目的:就是为了能够在合适的时机(窗口关闭和销毁 ),把这些对象统一进行释放。通过这个树形结构,就把界面上要显示的这些控件对象都组织起来了。
  • 这里的树上的这些对象,统一销毁是最好不过的,如果某个对象提前销毁,此时就会导致对应的控件就在界面上不存在了。

2. 认识对象模型(对象树)

2.1 什么是对象树

Qt 中创建很多对象的时候会提供一个 Parent 对象指针,下面来解释这个 parent到底是干什么的。

  • QObject 是以对象树的形式组织起来的。

  • 当创建一个 QObject 对象时,会看到 QObject 的构造函数接收一个 QObject 指针作为参数,这个参数就是 parent,也就是父对象指针。

  • 这相当于,在创建 QObject 对象时,可以提供一个其父对象,我们创建的这个 QObject 对象会自动添加到其父对象的 children()列表。

  • 当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)

**这种机制在 GUI程序设计中相当有用。**例如,一个按钮有一个 QShortcut(快捷键)对象作为其子对象。当删除按钮的时候,这个快捷键理应被删除。这是合理的。

  • Qwidget 是能够在屏幕上显示的一切组件的父类。
  • Qwidget 继承自 QObject,因此也继承了这种对象树关系。一个孩子自动地成为父组件的-个子组件。因此,它会显示在父组件的坐标系统中,被父组件的边界剪裁。例如,当用户关闭一个对话框的时候,应用程序将其删除,那么,我们希望属于这个对话框的按钮、图标等应该(-起被删除。事实就是如此,因为这些都是对话框的子组件。
  • 当然,我们也可以自己删除子对象,它们会自动从其父对象列表中删除。比如,当我们删除了个工具栏时,其所在的主窗口会自动将该工具栏从其子对象列表中删除,并且自动调整屏幕显示。

Qt 引入对象树的概念,在一定程度上解决了内存问题。

  • 当一个 QObject 对象在堆上创建的时候,Qt 会同时为其创建一个对象树。不过,对象树中对象的顺序是没有定义的。这意味着,销毁这些对象的顺序也是未定义的。

  • 任何对象树中的 QObject 对象 delete 的时候,如果这个对象有 parent,则自动将其从 parent的children() 列表中删除;如果有孩子,则自动 delete 每一个孩子。Qt 保证没有 QObject 会被delete 两次,这是由析构顺序决定的。

如果 QObject 在栈上创建,Qt 保持同样的行为。正常情况下,这也不会发生什么问题。来看下面的代码片段:

cpp 复制代码
{
	QWidget window;
	QLabel label("hello", &window); // 指定父类是widow
}

作为父组件的 window 和作为子组件的 label 都是 QObject 的子类(事实上,它们都是 Qwidget的子类,而 Qwidget 是 QObject 的子类)。这段代码是正确的,label 的析构函数不会被调用两次,因为标准 C++ 要求,局部对象的析构顺序应该按照其创建顺序的相反过程。因此,这段代码在超出作用域时,会先调用 label 的析构函数,将其从父对象 window 的子对象列表中删除,然后才会再调用 window 的析构函数。

  • 但是一旦我们的代码稍微修改一点就会出错
cpp 复制代码
{
	QLabel label("hello"); // 指定父类是widow
	QWidget window;
	label.setParent(&window);
}

情况又有所不同,析构顺序就有了问题。我们看到,在上面的代码中,作为父对象的 window 会首先被析构,因为它是最后一个创建的对象。在析构过程中,它会调用子对象列表中每一个对象的析构函数,也就是说,label此时就被析构了。然后,代码继续执行,在 window 析构之后,label也会被析构,因为 label也是一个局部变量,在超出作用域的时候当然也需要析构。但是,这时候已经是第二次调用 label的析构函数了,C++不允许调用两次析构函数,因此,程序崩溃了。

由此我们看到,Qt 的对象树机制虽然在一定程度上解决了内存问题,但是也引入了一些值得注意的事情。这些细节在今后的开发过程中很可能时不时跳出来烦扰一下,所以,我们最好从开始就养成良好习惯,即 在 Qt 中,尽量将其开辟在堆上,并指定好其 parent 父类对象

比如:

  • 如果我们把最初的代码改成在栈上开辟的话我们运行程序会发现什么都没有

Qt 对象树如下:

2.2 验证对象树

首先我们自定义一个label类,并在析构部分打上日志,如下步骤:

1. 选中工程名,鼠标右键------->"add new..."(或 "添加新文件" )

结果图如下:

  • 上面 Qt Creator 是帮我们生成了一些代码,但是没完全生成,头文件没有给我们主动包含,上面的头文件也是我自己手动包含的
  • 此时我们可以按F4来进行 .h文件 和 .cpp文件 来回切换

此时我们的 mylabel.cpp 中就是

cpp 复制代码
#include "mylabel.h"
#include <iostream>
 
MyLabel::MyLabel(QWidget* parent) : QLabel(parent)
{
}
 
MyLabel::~MyLabel()
{
    std::cout << "MyLabel 被销毁" << std::endl;
}

midget.cpp 中的代码就是

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"
#include "mylabel.h"
 
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this); // 将form file生成的界面和我们当前的widget进行关联起来
    // 使用自己定义的 MyLabel 代替原来的 QLabel,所谓的 "继承" 本质上是扩展,保持原有功能不变的基础上
    // 给对象扩展出一个析构函数,通过这个析构函数,打印一个自定义日志,方便观察程序运行结果
    MyLabel *label = new MyLabel(this);
    label->setText("Hello World");
 
}
 
Widget::~Widget()
{
    delete ui;
}
 
  • 此时我们运行代码,就可以看到窗口上有 Hello World 的字样,只要我们关闭窗口,就会输出我们的日志

这里也是验证了对象树自动释放对象的能力

  • 这里日志是有的,说明析构函数 是执行了,虽然没有 手动 delete ,但是由于把 MyLabel挂到了对象树上,此时窗口被销毁的时候,就会自动销毁对象树中的所有对象!!所以MyLabel的析构是执行到了

但是这里似乎出现了乱码的情况

  • 乱码问题出现的原因有且仅有一个 编码方式不匹配(不仅仅局限于 C++)
  • 比如字符串本身是 utf8编码的,但是终端(控制台)是按照 gbk的方式来解析显示的,就会出现乱码(相当于拿着 utf8的数值 去查询 gbk的 码表)

utf8 和 gbk

目前,表示汉字字符集, 主要是两种方式

  1. GBK(中国大陆) 使用 2 个字节表示一个汉字!Windows 简体中文版,默认的字符集就是 GBK
  2. UTF-8 / utf8 变长编码,表示一个符号,使用的字节数有变化,2-4但是在 utf8 中。一个汉字一般是 3 个字节
  3. Linux 默认就是 utf8

3. 解决编码问题

我们用文本文件打开 mylabel.cpp 文件,可以看到这个文件的编码方式

可看到这个文件的编码方式是 utf8,但是 Qt 的这个终端的编码方式肯定不是 utf8 ,但是Qt不支持修改编码方式,所以这里我们就需要借助 Qt 自己提供的打印日志的功能 qDebug,或者使用 QString 来处理编码方式。

cpp 复制代码
#include "mylabel.h"
#include <iostream>
 
#include <QtDebug> // 头文件
 
MyLabel::MyLabel(QWidget* parent) : QLabel(parent)
{
 
}
 
MyLabel::~MyLabel()
{
    // std::cout << "MyLabel 被销毁" << std::endl;
    qDebug() << "MyLabel 被销毁"; // qDebug 这个宏封装了 QDebug 对象,使用 qDebug 相当于使用 cout
}
cpp 复制代码
#define qDebug QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC).debug

此时中文就不会出现乱码情况,如下:

  • 后续在 Qt 中,如果想通过打印日志的方式,输出一些调试信息,都优先使用 qDebug。虽然使用 cout 也行,但是 cout 对于编码的处理不太好,在windows 上容易出现乱码(如果是 Linux 使用 Qt Creator , 一般就没事了,Linux 默认的编码一般都是 utf8)

  • 使用 qDebug, 还有一个好处:打印的调试日志是可以统一进行关闭的!!

  • 输出的日志,是开发阶段、调试程序的时候使用的。如果你的程序发布给用户,不希望用户看到这些日志的!!

  • qDebug可以通过编译开关,来实现一键式关闭

之前调试程序, 都是用调试器.VS/gdb,这里为啥要打印日志调试呢??

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

4. Qt 编程注意事项

4.1 Qt 中的命名事项

  • 类名:首字母大写,单词和单词之间首字母大写;

  • 函数名及变量名:首字母小写,单词和单词之间首字母大写

  • 起的名字要有描述性,不要使用 abc, xyz 这种比较无规律的名字来描述

  • 如果名字比较长,由多个单词构成的,就需要使用适当的方式来进行区分不同单词

  • 一般可以采用 蛇形命名法 或者 驼峰命名法

4.2 Qt Creator 中的快捷键

其里面内置 Vim 插件,因此我们也可以按照使用 Vim 操作来使用

  • 注释:ctrl+/

  • 运行:ctrl+R

  • 编译:ctrl+B

  • 字体缩放:ctrl+鼠标滑轮

  • 查找:ctrl+F

  • 整行移动:ctrl+shift+↑/↓

  • 帮助文档:F1

  • 自动对齐:ctrl+i

  • 同名之间的.h和.cpp 的切换:F4

  • 生成函数声明的对应定义:alt+enter

  • 跳转到控件定义: 鼠标左键 + ctrl,返回就是:alt + <-

4.3 使用帮助文档

打开帮助文档有三种方式,实际编程中使用哪种都可以

  1. 光标放到要查询的类名/方法名上,直接按 F1

  2. Qt Creator 左侧边栏中直接用鼠标单击"帮助"按钮

点击 "帮助" 之后,出现如下图:

3、找到 Qt Creator 的安装路径,在 "bin" 文件夹下找到 assistant.exe,双击打开

使用示例

  1. 新建项目,在新建的项目中使用 Qt 中的"QpushButton" 控件。

  2. 打开帮助手册,在"索引"里面输入"QpushButton":

注意:一定不要使用中文文档!!!

  • 阅读英文文档是每个程序员必备的专业技能,必须要练,不能退缩

  • Qt的文档从通俗易懂的角度来说,是技术类文档中非常出类拔萃的,只要大家稍微有点耐心,基本都能读懂个八九不十

5. 小结

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

  • 内存泄露/文件资源泄露

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

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

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

  • 如何在 Qt中打印日志,作为调试信息使用 cout固然可以, 但是并不是上策(字符编码处理的不好,也不方便统一进行关闭)Qt中推荐使用**qDebug()**完成日志的打印

相关推荐
青苔猿猿1 分钟前
(4)python中jupyter lab使用python虚拟环境
开发语言·python·jupyter·虚拟环境
彷徨而立14 分钟前
【C++】频繁分配和释放会产生内存碎片
开发语言·c++
code_shenbing30 分钟前
C# 实现列式存储数据
开发语言·c#·存储
我真的不会C42 分钟前
QT中的多线程
数据库·qt·系统架构
Kairo_011 小时前
在 API 模拟阶段:Apipost vs. Faker.js vs. Postman —— 为什么 Apipost 是最优选择
开发语言·javascript·postman
Once_day1 小时前
研发效率破局之道阅读总结(4)个人效率
开发语言·研发效能·devops
痕5171 小时前
如何在idea中写spark程序。
开发语言
橙子199110162 小时前
请简述一下什么是 Kotlin?它有哪些特性?
android·开发语言·kotlin
martian6652 小时前
信创系统图形界面开发指南:技术选择与实践详解
开发语言·科技·系统架构·系统安全·创业创新
我命由我123452 小时前
STM32 开发 - stm32f10x.h 头文件(内存映射、寄存器结构体与宏、寄存器位定义、实现点灯案例)
c语言·开发语言·c++·stm32·单片机·嵌入式硬件·嵌入式