博客主页:【夜泉_ly】
本文专栏:【暂无<>】
欢迎点赞👍收藏⭐关注❤️
文章目录
- [📚 前言](#📚 前言)
- [🛠️ 搭建环境](#🛠️ 搭建环境)
- [📂 新建项目](#📂 新建项目)
- [📝 初始代码理解](#📝 初始代码理解)
-
- main.cpp
- widget.h
- widget.cpp
- widget.ui
- HelloWorld.pro
- [🛠️ 中间文件](#🛠️ 中间文件)
📚 前言
本文主要内容:
嘿嘿,又开了一个新坑。
为什么学 Qt?
当 C++了解的差不多了,
我就想看看现在自己能做些什么实际的应用。
学习新技术也巩固了旧知识。
然后,我便发现了 Qt。
简单来讲,
Qt 能让我们从"面向黑框框编程"中解脱出来。
因为它提供了丰富的图形界面组件和工具,
使得我们可以更加直观地设计和开发具有良好用户体验的应用程序。
🛠️ 搭建环境
学Qt,那就必须有对应的Qt开发环境。
主要分三个部分:
- C++ 编译器
- Qt SDK(软件开发工具包)
- IDE
看起来很麻烦,但实际上,
我们安装一个Qt SDK,其他两个也都有了~~
这里贴个官网下载地址:http://download.qt.io/archive/qt/
很明显,这是个外国的网站。
怎么下载,那就八仙过海,各显神通了~~
我这里下的是 5.14.2
下载完成,双击安装,一路next,就行了?
不对!!先断网,再双击安装。
为什么?因为这样不用注册Qt账号😋。
安装完了大概会看到五个东西:
- Assistant ,这是官方文档
- Designer,这个后面会搭配Qt Creater使用
- L开头的,可以对国际化进行支持
- 黑框框,不管
- Qt Creater,Qt的集成开发工具,
后面主要就用这个
📂 新建项目
打开Qt Creater,新建文件:
我们想用Qt写一个GUI,因此:
点Application,再Widgets,再choose:
一路next到Details这里,选择QWidget
再next,最后到这个界面:
可以看到,这里已经有代码了。
Ctrl R
运行 ,甚至可以生成一个窗口,虽然啥也没有:
这里不得不再提一下Qt是什么了:
框架。。
这和我们以前从头开始造轮子就有点区别了。
在这里,我们看到一个完整的 Qt 项目框架已经被自动生成,甚至可运行。
比如刚刚我们选择了QWidget
,这里就自动帮我们搭好了相关的架子。
而我们要做的,就是在这个框架上面补充内容。
不过,为了避免当一个"框架驾驶员",
初学时,先来了解一下它提供了什么,加深理解。
📝 初始代码理解
main.cpp
接下来,先仔细看看 main.cpp
的东西:
cpp
#include "widget.h"
首先,包了一个"widget.h"
,
这个就是我们刚刚选了QWidgets
后自动生成的头文件。
cpp
#include <QApplication>
然后是<QApplication>
,这个是每个Qt应用程序的基础。
这里面有个类叫做 QApplication
,
作用是管理GUI应用程序的控制流和主要设置:
cpp
int main(int argc, char *argv[]){
main,这个不必多说。
而形参是命令行参数,这个暂时也不用管。
cpp
QApplication a(argc, argv);
创建了一个 QApplication
对象 a
,
用于管理我们的应用程序。
这个对象是编写 Qt 的图形化界面所必须要有的!
不过不需要我们写,毕竟是框架嘛😋。
cpp
Widget w;
w.show();
这个也是选择QWidgets
后自动生成的。
Widget w;
创建一个 Widget
对象。
Widget
是我们自定义的窗口类。
w.show();
显示窗口。
此调用将使 Widget
出现在屏幕上。
把这两句注释掉。。你的窗口就没了。
cpp
return a.exec();
}
exec()
的作用有很多:
不过我只看得懂第一句话🤣:
用来启动事件循环,直到被明确终止(比如点击右上角的叉)才会返回,然后退出程序。
如果改成return 0;
,那程序闪一下就没了:
widget.h
接下来看widget.h
,这里面就是 Widget
类的声明
cpp
#ifndef WIDGET_H
#define WIDGET_H
防止头文件被重复包含。
一般更推荐用#pragma once
,
不过它写都写了,那我也懒得改了😋~~
cpp
#include <QWidget>
#include <QWidget>
引入 QWidget 类,
这是 Qt 所有用户界面对象的基类。
cpp
QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
声明 Ui::Widget 类,这个是 Qt Designer 生成的类。
这个类的名字是可以自己改的,
不过要改的地方还挺多的,一般用他默认给的就行。
cpp
class Widget : public QWidget {
继承可以让人变得富有😁,类也一样。
QWidget
是创建项目时选择的父类。
不过需要注意,这里的 Widget
和上面的 Ui::Widget
不是一个东西!!!
这里的 Widget
是等会我们写代码的地方,通常继承自 QWidget
。
而 Ui::Widget
是由 Qt Designer 根据 .ui
文件生成的类。
为什么要用同一个名字?这样不会混吗?
其实不会,通过使用相同的名字,可以更好的体现两者的关联关系
------它们都用来管理和显示我们最终看到的界面。
cpp
Q_OBJECT
这是一个宏,后续使用信号和槽时必须要写这个宏。
Ctrl 加 鼠标点击
转到定义:
cpp
#define Q_OBJECT \
public: \
QT_WARNING_PUSH \
Q_OBJECT_NO_OVERRIDE_WARNING \
static const QMetaObject staticMetaObject; \
virtual const QMetaObject *metaObject() const; \
virtual void *qt_metacast(const char *); \
virtual int qt_metacall(QMetaObject::Call, int, void **); \
QT_TR_FUNCTIONS \
private: \
Q_OBJECT_NO_ATTRIBUTES_WARNING \
Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \
QT_WARNING_POP \
struct QPrivateSignal {}; \
QT_ANNOTATE_CLASS(qt_qobject, "")
。。。看不懂,不看了
cpp
public:
Widget(QWidget *parent = nullptr);
这里的 parent
,很明显代表父亲。
但为什么要在构造里面加个 parent
参数呢?
这就和Qt的对象树有关了,这个之后再讲~~
现在简单理解为:
这个参数将子控件挂到树上。
父控件被删除时,所有的子控件也会被自动删除。
这意味着指定父控件后,不必手动 delete
子控件。
cppp
~Widget();
析构,不必多说。
cpp
private:
Ui::Widget *ui;
};
ui
是 User Interface
的缩写,代表用户界面。
Widget
类有了 Ui::widget
类的指针,
就可以通过这个指针来管理和展示 UI 元素。
widget.cpp
接下来看看 widget.cpp
cpp
#include "widget.h"
#include "ui_widget.h"
引了两个头文件,
"widget.h"
好说,毕竟刚刚已经看过了。
而 "ui_widget.h"
,
这个是 Qt Designer 自动生成 的 UI 文件的头文件。
什么意思?
意思是不用我们管。。。
在项目文件管理窗口里都没有这个文件,
这样设计的目的就是避免我们直接修改它。
如果想修改这里面的内容,
正确的做法是打开 .ui
文件,通过图形化界面调整界面,
之后编译的时候会自动更新 ui_widget.h
。
cpp
Widget::Widget(QWidget *parent)
: QWidget(parent)
Widget
类的构造函数
这里用 QWidget
的指针,
调用了当前对象的父类 QWidget
的构造函数,
并将 parent
参数传过去。
而父类又会调用它的父类,
最终层层向上,来到了 QObject
的构造:
此时新创建的这个对象将被挂到对象树上,
便于统一的释放,减少内存泄漏的可能。
而对于这块,我简单模拟实现了一下Qt的对象树,
正确性未知,毕竟我没找到 QObject()
的实现,只找到了声明🤣:
下面来试试模拟实现:
cpp
#include <iostream>
#include <vector>
using namespace std;
class QObject
{
public:
QObject(QObject* parent = nullptr)
:_parent(parent)
{
if(parent)
{
parent->addNode(this);
cout << "Add a Node" << endl;
}
}
void addNode(QObject* child) { _child.push_back(child); }
virtual ~QObject()
{
cout << "Delete QObject and nodes" << endl;
for (auto e : _child)
{
if(e)
delete e;
e = nullptr;
cout << "Delete a child" << endl;
}
}
private:
QObject* _parent;
vector<QObject*> _child;
};
class QWidget : public QObject
{
public:
QWidget(QObject* parent = nullptr)
:QObject(parent)
{
cout << "QWidget()" << endl;
}
};
int main()
{
QObject a;
QWidget* b = new QWidget(&a);
QWidget* c = new QWidget(b);
return 0;
}
运行结果:
Add a Node
QWidget()
Add a Node
QWidget()
Delete QObject and nodes
Delete QObject and nodes
Delete QObject and nodes
Delete a child
Delete a child
上面的代码仅供参考。。。
如果对象是在栈上创建的,这段会直接报错(多次析构了)。
听说可以用智能指针解决,我这里就不写了。
cpp
, ui(new Ui::Widget)
{
ui->setupUi(this);
}
而之后,又 new 了一个 Ui::Widget
,用来设置 ui
。
为指定的小部件设置用户界面。
这里没有在Ui::Widget的构造中传入父对象的指针,为什么?
因为Ui::Widget就没有直接的父子管理能力!
对于Ui::Widget,可以理解为它主要是由图形化界面操控的,
所以和其他的QObject对象关系不大,因此没有父子关系。
再进一步讲,在设计原则中,有非常重要的六个字: "先描述,在组织" ,
在这儿就可以认为Ui::Widget负责描述,
而其他的QObject主要负责组织。
这样的设计模式可以方便我们将看到的界面和其背后应用程序的代码逻辑分离。
与之对应,其他的控件最好在构造时就传入父对象的指针,
不要等创建完了再 set
,不然有可能会导致析构被两次调用。
cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QPushButton pushButton;
Widget w;
pushButton.setParent(&w);
w.show();
return a.exec();
}
当退出时:
cpp
Widget::~Widget()
{
delete ui;
}
这个刚刚提过,ui
并没有在 Qt 的对象树上,因此这里需要手动 delete
。
widget.ui
再来看看这个 widget.ui
:
双击会来到一个图形化的界面:
在这里我们可以拖动左边的控件到中间的窗口,
然后可以在右边那个黄色的框里面设置对应的属性。
当我们再次编译时,widget.ui会自动生成对应的代码。
而点击左边的编辑,会来到widget.ui文件:
可以看见,这个文件只能在 设计模式 下更改,也就是刚刚的图形化界面。
然后,我们还能看见这个文件是 xml 格式的,
这说明了什么?
说明了只学过C/C++的我看不懂。。
HelloWorld.pro
最后是 HelloWorld.pro
cpp
QT += core gui
引入 Qt 的模块,以后可能会改这里。
现在?暂时不管~~
cpp
CONFIG += c++11
指定使用 C++11 标准进行编译。
如果没加这句,一些C++11的东西可能就用不了了。
cpp
SOURCES += \
main.cpp \
widget.cpp
HEADERS += \
widget.h
FORMS += \
widget.ui
SOURCES列出项目中所有的源文件。
HEADERS列出项目中所有的头文件。
FORMS列出项目中的 .ui
文件。
这个地方也是自动生成的,不用我们管。
🛠️ 中间文件
除了这些,Qt项目编译运行后,
还会多出一些中间文件。
一般会在与 项目目录 并列的地方,
多一个名字很像的目录(build-...
):
打开:
看到了熟悉的东西:Makefile
。
不过这个也是自动生成的,不用我们管。
而 ui_widget.h
,这就是自动生成的 widget.ui
的头文件!
因此在刚刚的 widget.ui
中引用的就是这个东西。
ui_widget.h
前面几句是注释,
比较有意思的是它还提示我们在该文件的任何修改都是无效的:
WARNING! All changes made in this file will be lost when recompiling UI file!
cpp
class Ui_Widget {
public:
这个类就是在 widget.h
中的 Ui::Widget
。
因为在后面的命名空间 Ui
中,
用一个叫做Widget
的空类继承了这个 Ui_Widget
。
cpp
void setupUi(QWidget *Widget)
{
if (Widget->objectName().isEmpty())
Widget->setObjectName(QString::fromUtf8("Widget"));
Widget->resize(800, 600);
retranslateUi(Widget);
QMetaObject::connectSlotsByName(Widget);
} // setupUi
在刚刚 Widget
的构造中,
Widget
对象将它的 this
指针传给了这个函数。
而这个函数的作用,
就是通过图形化界面中的内容,
对 Widget
对象进行初始化:
- if语句
检查 Widget 的对象名是否为空,
如果是,则设置对象名为 "Widget"。 - Widget->resize(800, 600);
调整窗口的大小到 800x600 像素。 - retranslateUi(Widget);
翻译? - QMetaObject::connectSlotsByName(Widget);
从名字猜作用:
connect连接 Slots槽 By通过 Name名字
cpp
void retranslateUi(QWidget *Widget);
这个应该是用来翻译的。
cpp
namespace Ui {
class Widget: public Ui_Widget {};
} // namespace Ui
这就是刚刚说的命名空间,
它用来将Ui相关的类都封在一个命名空间下,
可能便于理解?
比如我一看到 Ui::Widget
,噢,这是用来界面设计的。
再看到 Widget
,噢,这是用来写代码的。
.exe
最后再看个东西:
这个 exe
,就是最终生成的可执行程序,
和之前Qt Creater 中运行的效果一致。
希望本篇文章对你有所帮助!并激发你进一步探索编程的兴趣!
本人仅是个C语言初学者,如果你有任何疑问或建议,欢迎随时留言讨论!让我们一起学习,共同进步!