文章目录
- 一、概念简述
- 二、项目模板解析
-
- [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 的人来约定的,它的作用类似于一个自定义的应用层协议。
所以整个过程是这样的:
- Qt 使用
.ui这个 XML 文件,去描述程序的界面是什么样的。 - 构建时,
uic工具会依据这个 XML 文件,生成对应的 C++ 代码,也就是ui_widget.h。 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:
- 通过图形化界面设计
- 通过代码创建。
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设置位置 |
非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!🎉
