【QT】第一个 QT程序(对象树)

发布于:2025-02-22 ⋅ 阅读:(19) ⋅ 点赞:(0)

🌈 个人主页:Zfox_
🔥 系列专栏:Qt

一:🔥 QtHelloWorld程序

🦋 使⽤"标签"实现

纯代码⽅式实现

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

在这里插入图片描述

📚 实现效果:

在这里插入图片描述

void setText(const QString &);

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

  • QString 用起来其实也要比 std::string 好一点,因为 QString 内部已经对于字符编码做了处理,而不像 std::string 啥都没做

可视化操作实现

  1. 双击:"widget.ui"⽂件;
    在这里插入图片描述
  2. 拖拽"标签"⾄UI设计界⾯中,并双击修改标签内容;
    在这里插入图片描述

🔥 此时 ui 界面就会生成对应的 XML 格式代码,这个时候 qmake 就会根据这个 XML 代码生成对应的 C++ 代码,我们也可以在同目录下找到这个 C++ 代码
在这里插入图片描述

  1. 实现效果如下图所⽰:
    在这里插入图片描述

🦋 使⽤"按钮"实现

可视化操作实现

  1. 双击:"widget.ui"⽂件;
    在这里插入图片描述

  2. 拖拽控件⾄ui界⾯窗⼝并修改内容;
    在这里插入图片描述

  3. 构建并运⾏,效果如下所⽰:
    在这里插入图片描述

  • 这里的按钮的确可以点击,但是却没有任何反应,这个就设计到我们后面学的信号槽知识,后面会说的
  • QT 的信号槽机制:本质上就是给按钮的点击操作,关联上一个处理函数,当用户点击的时候,就会执行这个处理函数

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

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

在这里插入图片描述
📚 如下代码:

在这里插入图片描述

#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 中,设置成什么值,生成的变量名就叫啥名字就可以根据这个名字来获取到对应的控件的变量了

纯代码实现

widget.h

#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

#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 类的成员变量

🦋 内存泄露

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

  • 其实上述代码在 Qt 不会产生内存泄露, label 对象会在合适的时候被析构释放,之所以能够把对象释放掉,主要是因为把这个对象挂到了 对象树 上
  • 前端开发 (网页开发) 也涉及到 类似的 对象树 (DOM),本质上也是一个树形结构 (N 又树), 通过树形结构把界面上的各种元素组织起来
  • Qt 中也是类似,也是搞了一个对象树,也是 N 又树,把界面上的各种元素组织起来了

在这里插入图片描述

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

二:🔥 认识对象模型(对象树)

🦋 什么是对象树

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

  • QObject 是以对象树的形式组织起来的。
    • 当创建一个 QObject 对象时,会看到 QObject 的构造函数接收一个 QObject 指针作为参数,这个参数就是 parent,也就是父对象指针。
    • 这相当于,在创建 QObject 对象时,可以提供一个其父对象,我们创建的这个 QObject 对象会自动添加到其父对象的 children()列表。
    • 当父对象析构的时候,这个列表中的所有对象也会被析构。(注意,这里的父对象并不是继承意义上的父类!)

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

在这里插入图片描述
在这里插入图片描述

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

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

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

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

🌰 比如:

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

在这里插入图片描述

💻 Qt对象树如图:
在这里插入图片描述

🦋 验证对象树

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

  1. 选中工程名,鼠标右键 -------> “add new…”(或 “添加新文件” )… 建立出一个新的项目

在这里插入图片描述

  1. 点击"完成"之后,⼿动创建类的头⽂件以及源⽂件

在这里插入图片描述

  • 此时我们可以按 F4 来进行 .h 文件 和 .cpp 文件 来回切换
    在这里插入图片描述
  • 使用快捷键 Alt + Enter 可以实现快速定义

🦭 此时我们的 mylabel.cpp 中就是

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

🦭 midget.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

三:🔥 解决编码问题

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

在这里插入图片描述

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

#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
}
#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% 甚至更小要想调试,无法使用调试器了
  • 使用 日志 就可以很好的解决这种问题
  • 无论是哪种方式本质上都是观察程序执行的中间过程和中间结果~

四:🔥 Qt 编程注意事项

🦋 Qt 中的命名事项

  • 类名:首字母大写,单词和单词之间首字母大写;
  • 函数名及变量名:首字母小写,单词和单词之间首字母大写
  • 起的名字要有描述性,不要使用 abc, xyz 这种比较无规律的名字来描述
  • 如果名字比较长,由多个单词构成的,就需要使用适当的方式来进行区分不同单词
  • 一般可以采用 蛇形命名法 或者 驼峰命名法

🦋 Qt 中的快捷键

在这里插入图片描述
🔧 其里面内置 Vim 插件,因此我们也可以按照使用 Vim 操作来使用

  • 注释:Ctrl + /
  • 运行:Ctrl + R
  • 编译:Ctrl + B
  • 字体缩放:Ctrl + 鼠标滑轮
  • 查找:Ctrl + F
  • 整行移动:Ctrl + Shift + ↑/↓
  • 帮助文档:F1
  • 自动对齐: Ctrl + i
  • 同名之间的.h和.cpp 的切换:F4
  • 生成函数声明的对应定义:Alt + Enter
  • 跳转到控件定义: 鼠标左键 + Ctrl,返回就是:Alt + <-

五:🔥 Qt 析构函数执行顺序

mylabel.cpp

#include "mylabel.h"
#include <iostream>
 
#include <QtDebug>
 
MyLabel::MyLabel(QWidget* parent) : QLabel(parent)
{
    qDebug() << "mylabel.cpp 构造函数被调用";
}
 
MyLabel::~MyLabel()
{
    qDebug() << "mylabel.cpp 析构函数被调用";
}

widget.cpp

#include "widget.h" // 创建生成时的文件
#include "ui_widget.h"
#include <QLabel>  // 包含标签的头文件
#include "mylabel.h"
#include <QDebug>
 
 
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()
{
    qDebug() << "midget 析构函数被调用";
    delete ui;
}
 

🔧 运行结果如下:
在这里插入图片描述

  • 对象树确保的是先释放子节点的内存,后释放父节点的内存
  • 而析构函数的调用顺序则不一定遵守上述要求,因此看到子节点的析构执行顺序反而在父节点析构顺序之后

注意:调用析构函数和释放内存并非是同一件事情。

六:🔥 共勉

🦋 小结

  1. 认识 QLabel 类,能够在界面上显示字符串。通过 setText 来设置的,参数 QString(Qt 中把 C++ 里的很多容器类,进行了重新封装,历史原因)
  2. 内存泄露 / 文件资源泄露
  3. 对象树Qt 中通过对象树,来统一的释放界面的控件对象,Qt 还是推荐使用 new 的方式在堆上创建对象,通过对象树, 统一释放对象创建对象的时候,在构造函数中, 指定父对象 (此时才会挂到对象树上),如果你的对象没有挂到对象树上,就必须要记得手动释放 !!
  4. 通过继承自 Qt 内置的类,就可以达到对现有控件进行功能扩展效果 Qt 内置的 QLabel,没法看到销毁过程的。为了看清楚,就创建类 MyLabel, 继承自 QLabel 重写析构函数。在析构函数中加上日志,直观的观察到对象释放的过程了,也可以重写控件中的任何功能。不仅仅是析构函数, 达到功能扩展目的
  5. 乱码问题 和 字符集 ~ MySQL (很多地方都涉及到)
  6. 如何在 Qt 中打印日志,作为调试信息使用 cout 固然可以, 但是并不是上策(字符编码处理的不好,也不方便统一进行关闭)Qt 中推荐使用 qDebug() 完成日志的打印

😋 以上就是我对 【Qt】新世界大门:概述 的理解, 觉得这篇博客对你有帮助的,可以点赞收藏关注支持一波~ 😉
在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到