Qt Widgets入门:项目模板解析与Hello World

文章目录

  • 一、概念简述
  • 二、项目模板解析
    • [2.1 main.cpp](#2.1 main.cpp)
    • [2.2 widget.h](#2.2 widget.h)
    • [2.3 widget.cpp](#2.3 widget.cpp)
    • [2.4 widget.ui](#2.4 widget.ui)
    • [2.5 .pro 文件](#2.5 .pro 文件)
  • [三、Hello World](#三、Hello World)
    • [3.1 方式一:图形化界面](#3.1 方式一:图形化界面)
    • [3.2 方式二:纯代码](#3.2 方式二:纯代码)
    • [3.3 QString:Qt 自己的字符串](#3.3 QString:Qt 自己的字符串)
    • [3.4 对象树与内存管理](#3.4 对象树与内存管理)
    • [3.5 验证对象树释放内存](#3.5 验证对象树释放内存)
      • [3.5.1 日志乱码的问题](#3.5.1 日志乱码的问题)
    • [3.6 输入框:Line Edit](#3.6 输入框:Line Edit)
    • [3.7 按钮:Push Button 与信号槽](#3.7 按钮:Push Button 与信号槽)
      • [3.7.1 图形化界面方式](#3.7.1 图形化界面方式)
      • [3.7.2 纯代码方式](#3.7.2 纯代码方式)
  • [四、图形化设计 vs 纯代码](#四、图形化设计 vs 纯代码)
  • [五、Qt 的命名规范与坐标体系](#五、Qt 的命名规范与坐标体系)
    • [5.1 命名规范](#5.1 命名规范)
    • [5.2 坐标体系](#5.2 坐标体系)
  • 六、总结

一、概念简述

  Qt是一个跨平台的 C++图形界面(GUI)开发框架。本文从创建一个 Qt Widgets Application项目开始,带大家:

  • 看懂模板自动生成的几个文件,知道它们各自的作用。
  • 用多种方式实现 hello world:图形化设计、纯代码、输入框、按钮。
  • 理解 Qt 里两个最核心的概念:对象树和信号槽(初识)。
  • 了解 Qt 的命名规范和坐标体系。

  本文先"会用",很多细节(比如对象树、信号槽的原理)后面的文章再深入,看不懂也没关系,先有个印象。

二、项目模板解析

  使用 Qt Widgets Application模板创建项目,Qt Creator 会自动生成这几个文件:main.cpp、widget.h、widget.cpp、widget.ui和 .pro文件。下面一个一个来看。

2.1 main.cpp

cpp 复制代码
#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return QApplication::exec();
}
  • argc、argv:命令行参数的个数和内容,和普通 C++ 程序一样。
  • QApplication:编写 Qt 图形界面程序,必须要有这个对象。它负责管理应用程序的资源、事件等。
  • Widget w:窗口对象。Widget是创建项目时,填写的类名,它的父类是 QWidget。
  • w.show():让控件显示出来。show是 QWidget提供的方法,Widget继承了它。
  • QApplication::exec():让程序跑起来 ,进入事件循环,等待并处理用户的各种操作(点击、输入等),直到窗口关闭才返回。

这里的 exec 和 Linux 里的 exec 有关系吗?

没有任何关系,仅仅是名字一样。Linux 里的exec系列函数是进程程序替换,把可执行文件替换到当前进程中;而这里的exec只是让 Qt 程序进入事件循环。

2.2 widget.h

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:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget() override;

private:
    Ui::Widget *ui;
};
#endif // WIDGET_H
  • #ifndef和 #define:防止头文件被重复包含。
  • QWidget:创建项目时选择的父类。Qt 有一个很贴心的设定:使用 Qt 内置的类,需要包含的头文件名字就和类名一致(QWidget、QApplication、QLabel......)。
  • Q_OBJECT:Qt 内置的宏。某个类要使用信号和槽,就需要引入这个宏 。它会配合 Qt 的元对象编译器(moc)生成一堆额外的代码。
  • parent:Qt 引入了对象树 ,创建出来的 Qt 对象可以挂到对象树上,往上挂的时候就需要指定父节点,一个父节点可以有 N 个子节点。(后面会详细讲)
  • Ui::Widget *ui:和 .ui文件(form file)密切相关。

2.3 widget.cpp

cpp 复制代码
#include "widget.h"
#include "ui_widget.h"

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

Widget::~Widget()
{
    delete ui;
}
  • ui_widget.h:这个头文件在项目目录里是找不到的,它是在构建时,由 Qt 的 uic工具根据 widget.ui生成的。
  • ui->setupUi(this):作用是把图形化设计出来的界面 和当前的 Widget对象关联起来,把设计的控件真正创建出来。
  • 析构函数中 delete ui,把 ui对象释放掉。

2.4 widget.ui

  双击 widget.ui,可以进入设计窗口:

  • 左边是 Qt 内置的控件列表。
  • 中间是将来生成的程序窗口,可以把左侧的控件拖拽到窗口中,直接创建静态界面。
  • 右上角是对象查看器,显示当前窗口上有哪些控件。
  • 右下角是选中控件的属性。

  如果用文本方式打开 widget.ui,可以发现它其实是一个 XML 文件。

XML 和 HTML 有什么区别?

两者都是用成对的标签来表示数据。区别在于:HTML 的每个标签的含义是固定的,所有浏览器都统一遵守这套规则;而 XML 有哪些标签、每个标签是什么含义,都是由程序员自己定义的。这里的标签就是开发 Qt 的人来约定的,它的作用类似于一个自定义的应用层协议。

  所以整个过程是这样的:

  1. Qt 使用 .ui这个 XML 文件,去描述程序的界面是什么样的。
  2. 构建时,uic工具会依据这个 XML 文件,生成对应的 C++ 代码,也就是 ui_widget.h。
  3. widget.cpp中的 setupUi调用这些代码,把完整的页面构造出来。

  ui_widget.h在哪里能看到呢?它在构建目录下(build-xxx-Debug之类的目录),按下面的操作可以找到:

2.5 .pro 文件

bash 复制代码
QT += widgets

CONFIG += c++17

# You can make your code fail to compile if it uses deprecated APIs.
# In order to do so, uncomment the following line.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000

SOURCES += \
    main.cpp \
    widget.cpp

HEADERS += \
    widget.h

FORMS += \
    widget.ui

# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

  .pro文件的名字对应项目名(如test.pro),它是 Qt 项目的工程文件 ,也是 qmake工具构建项目时的重要依据。

  • QT += widgets:要引入的 Qt 模块,可以按需修改。(core、gui默认就已经包含了)
  • CONFIG += c++17:编译选项,指定 C++ 标准。
  • SOURCES、HEADERS、FORMS:描述了当前项目中,参与构建的源文件、头文件、界面文件都有哪些。这几项不需要手动修改,Qt Creator 会自动维护。
  • 最后的部署规则一般不用管。

  .pro的作用类似于 Linux 中的 Makefile。其实最终还是会用到 Makefile,只不过是由 qmake根据 .pro自动生成的,不需要我们自己写。Qt Creator 把这些编译细节都封装好了,很方便。构建完成后,在构建目录下的 debug目录里,就能找到最终的可执行程序(exe)。

三、Hello World

  下面用两种方式实现 hello world:

  1. 通过图形化界面设计
  2. 通过代码创建。

3.1 方式一:图形化界面

  从控件列表中,把 Label拖到窗口中,右上角的对象查看器里会显示当前有哪些控件。修改它的文本为 hello world:

  此时在 .ui文件里多出了对应的 XML 代码,这是 Qt 自动完成的:

  编译运行后,ui_widget.h文件里也跟着发生了改变,这就是根据 .ui的 XML 内容生成的:

3.2 方式二:纯代码

  通过代码来构建界面时,通常会把构建界面的代码放在 Widget(或MainWindow)的构造函数 中。在 Widget的构造函数里加上下面的代码:

cpp 复制代码
#include <QLabel>      // 需要包含 QLabel 头文件

QLabel* label = new QLabel(this);   // this:把 label 挂到当前对象的对象树下
label->setText("hello world");
  • setText:设置控件中要显示的文本。
  • 默认情况下,标签显示在窗口的左上角。如果想放在其他位置,可以通过代码调整(后面讲坐标时会介绍)。
  • new QLabel(this)中的 this是父对象。可以不指定父对象,但指定有好处(后面讲对象树时会讲)。

3.3 QString:Qt 自己的字符串

  细心的话会发现:setText的参数明明是 QString,我们传进去的却是字符串字面量 "hello world"。这是因为字符串被隐式转换成了 QString。

  QString是 Qt 自己"造的一套轮子",同样的还有 QVector、QList、QMap等容器。

为什么 Qt 要自己造一套,而不直接用 C++ 标准库?

  C 风格的字符串(以\0结尾)和 C++ 的 std::string用起来都不太方便,尤其是在字符编码(比如中文)的处理上。而 QString已经对字符编码做了处理 ,用起来更省心。
  QString也提供了接收 C 风格字符串的构造函数,所以可以不用显式构造 QString。

  • 开发中可以用 C++ 标准库那一套,也可以用 Qt 这一套。而Qt 的原生接口(API)里,大多使用的是 Qt 自己的这套容器。

3.4 对象树与内存管理

上面的代码里 new 了一个 QLabel,却没有 delete,这会不会内存泄漏?

  内存泄漏是一个很严重的问题,写 C++ 时,看到 new 就要想想 delete 在哪,这个意识要刻进 DNA 里。
  但上面的代码,在 Qt 中并不会内存泄漏。原因是:创建 QLabel时传入了 this,把这个对象挂到了对象树上 ,会在合适的时候被释放。

  对象树 是一个 N 叉树,把界面上的各种元素组织起来:

  • 一个对象挂到对象树上后,当它的父对象被销毁 时,会自动销毁它所有的子对象(递归地)。
  • 这样的组织方式,方便把界面上的这些对象统一释放。
  • 所以控件要使用 new在堆上创建,把生命周期交给 Qt 的对象树统一管理。

如果不指定父对象会怎么样?

控件不会显示在当前窗口里(单独调用show的话会成为一个独立窗口),也不会被对象树自动释放,需要自己负责管理。

那在栈上创建控件,会有什么问题?

可以做个实验,把代码改成下面这样:

cpp 复制代码
// QLabel* label = new QLabel(this);
QLabel label(this);
label.setText("hello world");

运行之后,界面上什么也看不到。 因为栈上的 label在构造函数结束时就被销毁了,自然无法再显示出来。

3.5 验证对象树释放内存

  思路:封装一个 MyLabel类继承自 QLabel,在析构函数里打印日志,再把它挂到对象树上,看它会不会被释放。

mylabel.h:

cpp 复制代码
#pragma once
#include <QLabel>

class MyLabel : public QLabel
{
public:
    MyLabel(QWidget* parent);
    ~MyLabel();
};

mylabel.cpp:

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

MyLabel::MyLabel(QWidget *parent) : QLabel(parent) {}

MyLabel::~MyLabel()
{
    qDebug() << "MyLabel 被释放";
}

在 Widget的构造函数中使用:

cpp 复制代码
MyLabel* hw = new MyLabel(this);
hw->setText("hello world");

运行后,关闭窗口,就会看到 "MyLabel 被释放" 的日志------说明 Widget销毁时,把它的子对象 MyLabel一并释放了。

3.5.1 日志乱码的问题

  如果在析构函数里直接用 cout << "MyLabel 被释放" << endl;打印中文,输出可能是乱码。

为什么会乱码?

  编码方式有很多种,常见的有两种:
  ① GBK(Windows 默认编码):中文占 2 个字节。
  ② UTF-8(Linux 默认编码):中文一般占 3 个字节。
  如果字符串本身是 UTF-8 编码的,但终端(控制台)是按照 GBK 的方式来解析显示的,就会出现乱码。 字符串的编码,和当前源文件的编码方式直接相关。

  查看文件的编码:用记事本打开源文件,点击"文件 → 另存为",左下角会显示当前是 UTF-8 还是 GBK。

  解决办法:使用 Qt 自己的工具,它们会自动处理编码:

  • 使用 QString来处理字符串。
  • 使用 Qt 专门的日志打印工具 qDebug()(需要包含 QDebug头文件)。
cpp 复制代码
#include <QDebug>
qDebug() << "MyLabel 被释放";    // 可以像 cout 一样使用
  • qDebug()其实是一个宏 ,封装了 QDebug对象,用法和 cout类似。
  • 细节:用 qDebug()做日志输出还有一个好处:可以统一关闭(定义QT_NO_DEBUG_OUTPUT宏),不用一个一个去删。

3.6 输入框:Line Edit

  hello world还可以用很多种方式实现,下面简单介绍一些可以交互的方式。先用编辑框:文本框里的内容是可以编辑的。

  • 图形化界面 :拖一个 Line Edit控件到窗口,设置它的文本。
  • 纯代码 :在 Widget构造函数中添加:
cpp 复制代码
QLineEdit* edit = new QLineEdit(this);
edit->setText("hello world");

3.7 按钮:Push Button 与信号槽

  按钮有很多种,这里使用最普通的 Push Button。设计页面的方式和上面差不多。但怎么让点击按钮做出反应呢?这里就要用到 Qt 最核心的机制:信号槽。

  使用 connect函数来把信号和槽连接起来。注意:Qt 中这个 connect是"连接信号和槽",和 TCP 建立连接没有任何关系。

connect 到底是做什么的?

  它是用来确定关系的,需要回答四个问题:
  ① 哪个控件 发信号?
  ② 发什么信号 ?
  ③ 谁 来处理这个信号?
  ④ 怎么 处理这个信号?

3.7.1 图形化界面方式

  在图形化界面中设计了按钮,在代码里怎么访问它呢?通过 ui这个对象,ui->按钮的对象名就能拿到:

只需要在 Widget的构造函数中添加 connect:

cpp 复制代码
connect(ui->pushButton, &QPushButton::clicked, this, &Widget::handleClick);

对应前面的四个问题:

  • ui->pushButton:这个控件发出信号。
  • &QPushButton::clicked:发出的是"点击"信号。
  • this:当前这个类的对象来处理。
  • &Widget::handleClick:处理的方法是 Widget::handleClick。

注意:要在 widget.h 里添加这个成员函数的声明,然后在 widget.cpp中实现:

cpp 复制代码
void Widget::handleClick()
{
    if (ui->pushButton->text() == "hello world")
        ui->pushButton->setText("hello qt");
    else
        ui->pushButton->setText("hello world");
}
  • 细节:处理信号的函数就叫槽函数。Qt 5 以后用上面这种函数指针的写法,普通的成员函数就可以作为槽函数 ;也可以写在private slots:下面。

3.7.2 纯代码方式

cpp 复制代码
button = new QPushButton(this);    // button 是 Widget 的成员变量
button->setText("hello world");
connect(button, &QPushButton::clicked, this, &Widget::handleClick2);
cpp 复制代码
void Widget::handleClick2()
{
    if (button->text() == "hello world")
        button->setText("hello qt");
    else
        button->setText("hello world");
}

为什么这里要把 button 定义成成员变量?

如果 button只是构造函数里的局部变量 ,那么在 handleClick2里是访问不到的。所以要把它定义成 Widget的成员变量,在 widget.h中加上 QPushButton* button;。

  • 对比一下:代码创建 的按钮对象,需要自己 new,而且需要被其他函数访问,所以要设为成员变量;图形化 方式创建的控件,Qt 已经帮我们生成好了,它是 ui对象的成员变量。
  • 细节:如果不想定义成员变量,也可以用 lambda 表达式作为槽函数:connect(button, &QPushButton::clicked, this, [=]() { ... });

四、图形化设计 vs 纯代码

怎么选择:图形化工具,还是纯代码构建?

  ① 如果页面内容比较固定 ,使用图形化方式构建会更方便。
  ② 如果页面需要动态变化 (比如根据数据动态增删控件),使用代码构建会更容易维护。
  实际开发中,两者往往是结合使用的。

五、Qt 的命名规范与坐标体系

5.1 命名规范

  Qt 中的命名规范:

  • 命名要有描述性。
  • 多个单词构成的名字,需要用恰当的方式区分单词。Qt 中使用的是用大写字母来区分单词,比如 QApplication、QWidget。
  • 大驼峰(每个单词首字母大写)一般用于类名 ,小驼峰(第一个单词首字母小写)一般用于变量和函数,比如handleClick、setText。

5.2 坐标体系

  Qt 使用的是平面直角坐标系,但和数学中不同的是:纵轴方向是从上往下增长的。

  • 左上角为原点 (0, 0)。
  • 横轴向右增长,纵轴向下增长。
  • 坐标的单位是像素。

  一个控件的坐标,是相对于父控件(父窗口)的,所以不同的控件可能有不同的坐标系。而 QWidget作为顶层窗口,没有父元素,它的坐标系就是整个屏幕(显示器)。

  控件默认在 (0, 0)的位置,可以通过 move来设置位置:

cpp 复制代码
button->move(200, 300);    // 给控件设置位置,水平方向 200,竖直方向 300(单位:像素)
this->move(100, 0);        // 给窗口本身设置位置(相对于屏幕)

六、总结

  本文通过 hello world梳理了 Qt Widgets 项目的基础:

内容 要点
模板文件 main.cpp:QApplication+ 窗口对象 + exec事件循环;.ui(XML)经 uic生成 ui_widget.h;.pro描述项目构建
两种创建界面的方式 图形化(适合固定页面)、纯代码(适合动态页面)
对象树 控件 new在堆上并指定父对象,由父对象统一释放,避免内存泄漏
信号槽 connect(发送者, 信号, 接收者, 槽函数)确定响应关系
字符串与日志 用 QString和 qDebug()避免编码乱码
坐标体系 左上角为原点,纵轴向下,相对于父控件,move设置位置

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!🎉

相关推荐
bksczm1 小时前
Redis 持久化:RDB 快照与 AOF 日志
数据库·redis·mybatis
码途行者1 小时前
网站的SQL 注入:从攻击原理到防御措施
开发语言·sql·php
.痞老板.1 小时前
mysql初阶--数据库设计
数据库·mysql·oracle
旖旎夜光2 小时前
【C++ 基础入门】C++ 多态(中):虚函数表、多态原理与动静绑定
开发语言·c++·笔记·学习
你看我名字2 小时前
产品参数昨天还对,今天已过期:数眼智能与大模型应用如何做资料版本校验
开发语言·架构
老秦包你会3 小时前
计算机网络第二课 ----Socket(套接字)编程预备
开发语言·计算机网络·php
旖旎夜光3 小时前
【C++ 基础入门】C++多态(下):单/多继承虚表、多态补充与 21 道高频题
开发语言·c++·笔记·学习
Decay__3 小时前
SQL Server 数据库全栈运维实战系列 — 合集索引(24 篇)
运维·数据库·sql·mysql·架构
Thanks_ks3 小时前
SQL 的 GROUP BY 与聚合函数:从会写 SELECT 到会做统计
数据库·sql·mysql·group by·数据统计·聚合函数·having