
前言Qt是一个强大的跨平台开发工具主要是用来做客户端开发是非常值得学习的开发工具所以我决定开坑Qt。1.初识QtQt是一个跨平台的应用程序开发框架主要使用C进行开发。我们可以利用它提供的类和接口创建窗口、按钮、标签等控件搭建一个能够和用户交互的图形界面。比如我们希望在窗口上显示一段文字就可以使用标签控件希望用户点击某个位置触发操作就可以使用按钮控件。不需要每次都从最底层去实现这些功能Qt已经把很多常用功能封装好了。Qt并不只是用来画窗口的它还提供了文件处理、网络通信等功能。支持的平台也不只有Windows还包括Linux、macOS以及一些移动端和嵌入式平台。不过跨平台通常是指代码可以在不同平台上复用具体可以看到 Qt官方介绍开始使用时我们还会接触到Qt Creator和Qt Designer。这几个名字看起来很接近但它们不是同一个东西名称作用Qt开发框架提供编写程序所需要的类和功能Qt Creator开发工具用来编辑代码、构建、运行和调试项目Qt Designer界面设计工具通过拖拽控件、修改属性来设计界面Qt Creator中集成了界面设计功能所以我们可以在同一个工具里切换代码编辑和界面设计。我这里使用的是Qt Widgets项目通过C类和控件来构建界面。项目采用.pro文件配合qmake进行构建下面也就围绕这套项目结构来介绍。2.认识基本项目代码创建一个项目以后目录里会出现头文件、源文件、界面文件和项目文件。先把它们各自负责的事情分清楚后面看代码就不会觉得它们是混在一起的。文件主要作用main.cpp程序入口创建应用对象和窗口进入事件循环widget.h、widget.cpp声明和实现我们自己的窗口类widget.ui保存界面的布局和属性等信息Project_demo.pro记录项目使用的模块、源文件等构建信息mylabel.h、mylabel.cpp声明和实现后面用于观察析构过程的自定义标签类2.1main函数与事件循环先来看main.cpp#includewidget.h#includeQApplicationintmain(intargc,char*argv[]){QApplicationa(argc,argv);Widget w;w.show();returna.exec();}这里的main依旧是C程序的入口。argc和argv就是命令行参数这里在之前的文章也有过介绍创建QApplication对象时会把它们传进去。QApplication a(argc, argv);创建的是应用对象它负责Qt Widgets应用所需要的一些初始化和事件处理工作。对于这样的Widgets程序应用对象要先于窗口和其他控件创建。一个程序中也不需要给每个窗口分别创建一个QApplication。QApplication文档接下来是Widget w;它创建了一个窗口对象。但创建对象和把窗口显示出来是两件事所以后面还要调用w.show();。这里要区分两个名字QWidget是Qt提供的类。Widget是我们自己定义的类它继承了QWidget。Widget这个名字不是固定要求只是当前项目把窗口类取成了这个名字。最后的a.exec()是什么意思呢以前写控制台程序代码从上往下执行执行到最后就结束了。但一个图形界面程序不能刚把窗口显示出来就退出它还需要等待用户点击、输入以及处理窗口重绘等事情。具体细节后面在去介绍2.2窗口类的声明再来看widget.h#ifndefWIDGET_H#defineWIDGET_H#includeQWidgetQT_BEGIN_NAMESPACEnamespaceUi{classWidget;}QT_END_NAMESPACEclassWidget:publicQWidget{Q_OBJECTpublic:Widget(QWidget*parentnullptr);~Widget();private:Ui::Widget*ui;};#endif// WIDGET_H最外面的#ifndef、#define和#endif是头文件保护避免同一个头文件被重复包含时引起重复定义等问题。class Widget : public QWidget表示我们的Widget继承自QWidget。这样它就具备了窗口和控件的基本功能前面调用的show()也是继承得到的不需要我们自己实现一遍。构造函数接收一个QWidget* parent默认值是nullptr。这个参数用来指定父对象具体在后面介绍对象树时会介绍。最后再看Ui::Widget *ui;。前面的namespace Ui { class Widget; }是在Ui命名空间中前置声明了另一个Widget类。但是这里Ui::Widget与我们定义的Widget是两个不同的类。前者是根据界面文件生成的辅助类后者才是我们正在编写的窗口类。这里先声明一个指向辅助对象的指针具体创建和使用放在widget.cpp中。3.界面创建与项目构建3.1通过Designer设计界面在Qt Creator中打开widget.ui就可以进入界面设计界面左边是控件箱中间是设计区域右边可以查看对象以及修改属性。比如想让窗口显示Hello World就可以拖入一个QLabel再修改它的text属性。这种方式不需要我们手动写出每个控件的创建代码。不过拖拽操作也不是凭空生成界面它的结果会保存在.ui文件中。切换到文本方式查看当前项目的widget.ui是这样的?xml version1.0 encodingUTF-8?uiversion4.0classWidget/classwidgetclassQWidgetnameWidgetpropertynamegeometryrectx0/xy0/ywidth800/widthheight600/height/rect/propertypropertynamewindowTitlestringWidget/string/property/widgetresources/connections//ui它使用的是XML格式。我虽然不会XML也可以先对照内容看出这里描述了一个QWidget尺寸是800×600窗口标题是Widget。目前这个文件描述的还是一个空窗口没有标签。如果我们通过Designer添加标签并保存文件里就会多出相应控件及其属性的信息。后面代码中出现的Hello World!则是我们通过C代码添加的。那XML是怎么变成C界面的呢构建时Qt的uic工具会读取widget.ui生成ui_widget.h。里面包含Ui::Widget这样的辅助类以及用于初始化界面的setupUi()函数。窗口的构造函数调用它就能按照界面描述设置窗口并创建其中定义的控件。UI文件的使用方式也就是说Designer帮我们完成了界面描述生成工具再把这些描述转成程序能够使用的代码。3.2通过代码创建标签另一种方式就是直接写代码创建控件。先看最简单的标签写法。下面这两行放在窗口构造函数中就可以创建一个属于当前窗口的QLabel并设置文字QLabel*labelnewQLabel(this);label-setText(Hello World);使用QLabel时需要包含它的头文件QLabel。QLabel是标签控件可以用来显示文本等内容setText()就是设置它显示的文字。这里传入的this指向当前窗口对象让父对象管理子对象setText()接收的是QString。它是Qt提供的字符串类这里的字符串字面量在默认配置下可以转换成QString所以可以直接写成Hello World。后面使用Qt接口时我们还会经常接触到这个类型。QLabel文档当前项目为了继续观察对象的析构过程我又把普通QLabel替换成了继承自它的MyLabel。完整的widget.cpp如下#includewidget.h#includeui_widget.h#includeQLabel#includemylabel.hWidget::Widget(QWidget*parent):QWidget(parent),ui(newUi::Widget){ui-setupUi(this);// QLabel* label new QLabel(this);// label-setText(Hello World);MyLabel*labelnewMyLabel(this);label-setText(Hello World!);}Widget::~Widget(){deleteui;}两种写法是替换关系不要在同一个作用域中同时定义两个叫label的变量。我们顺着构造函数看一遍QWidget(parent)先初始化基类部分并把父对象参数传给基类。ui(new Ui::Widget)创建界面辅助对象。ui-setupUi(this)把界面文件描述的内容设置到当前窗口上。new MyLabel(this)创建一个标签子控件再通过setText()设置它的内容。这里同时使用了界面文件和手写代码.ui负责空窗口的尺寸、标题等设置代码负责添加标签。这两种方式可以配合使用并不是只能选其中一种。还有一个容易疑惑的地方label明明是构造函数里的局部变量为什么构造函数结束后标签还能留在窗口上因为label只是一个局部的指针变量标签对象本身是通过new创建在堆上的。构造函数结束指针变量的生命周期结束但它指向的对象不会因此自动销毁。那这个对象什么时候释放呢这就和后面的对象树有关。这里再额外补充一点其实也可以在栈上定义对象但这种方式不推荐虽然在之前C文章里介绍过函数栈结束时会自动调用它的析构函数释放资源但有时候可能会出现控件提前销毁的情况所以对于Qt来说最安全的方式是通过this指针交给父对象进行管理3.3pro文件与自动生成的文件界面和代码都写好后项目还需要构建才能运行。当前项目使用的Project_demo.pro就是qmake读取的项目文件。先看其中的模块和语言标准配置QT core gui greaterThan(QT_MAJOR_VERSION, 4): QT widgets CONFIG c11QT core gui表示使用相应的Qt模块。后面的条件是在Qt主版本大于4时加入widgets模块CONFIG c11则表示使用C11相关编译配置。再看文件列表SOURCES \ main.cpp \ mylabel.cpp \ widget.cpp HEADERS \ mylabel.h \ widget.h FORMS \ widget.ui这几个名字就比较直观了SOURCES项目中的源文件。HEADERS项目中的头文件。FORMS需要处理的界面文件。.pro不是普通C代码里面的是在给对应的项目变量追加内容。把文件加入项目时也需要让构建系统知道这些文件的存在。具体参考文档在这里**qmake文档把关系连起来看就是下面这样构建以后打开对应的构建目录会看到不少原先没有手写过的文件比如ui_widget.h是根据界面文件生成的供widget.cpp包含和使用。moc_widget.cpp是moc生成的代码也要参与编译和链接。.o文件是编译产生的目标文件。Project_demo.exe是Windows下最终得到的可执行程序。把这些文件和“运行后”联系在了一起是因为点击运行时开发工具通常会先完成必要的构建。准确来说它们是在配置、编译和链接等阶段产生的不是窗口程序运行时才生成。自动生成的文件可以打开观察但一般不要直接修改。比如改了ui_widget.h下一次重新生成时改动就可能被覆盖。要改变界面应该回到widget.ui或者自己的源文件中修改。4.对象树与对象的释放4.1父对象与子对象回到前面的代码MyLabel*labelnewMyLabel(this);我们通过new创建了一个对象却没有在窗口析构函数中写delete label;这样会不会内存泄漏呢对于这里的写法标签的释放交给了Qt的父子对象管理机制。Qt中的QObject支持父子对象关系QWidget及其派生控件也具备这种能力。一个对象可以有多个子对象子对象下面还可以继续有自己的子对象这样连接起来就形成了对象树有些像是一颗多叉数。当一个父对象析构时会删除它仍然拥有的子对象子对象的析构又会继续清理它自己的子对象。对于窗口控件来说父子关系还和界面上的包含关系有关例如标签可以显示在父窗口内部。Qt对象树与所有权所以new MyLabel(this)不只是指定标签显示在哪个窗口里也让当前窗口承担了这个标签的所有权。这里要把两种关系分开MyLabel : public QLabel是类的继承关系。标签对象的parent指向窗口对象才是对象之间的父子关系。窗口对象不是标签对象的“基类对象”它们是两个独立创建出来的对象。前面还出现了ui那它是不是也挂在对象树上呢其实不是。Ui::Widget只是一个普通的界面辅助类并不是QObject。 它虽然和我们自己的Widget名字相近但用途不同。因此窗口析构函数中的这段代码Widget::~Widget(){deleteui;}释放的是new Ui::Widget创建的辅助对象。这里的delete ui;并不是在删除标签。真正清理标签的是窗口对象后续的基类析构过程。也就是我们自己的Widget析构函数执行结束后QWidget等基类的析构流程继续处理子对象。对象树也不是一个能管理所有内存的垃圾回收器。没有指定父对象的堆对象仍然需要自己安排释放普通的new int等对象也不会因为程序使用了Qt就自动挂进对象树有些像是类GC的意思。4.2通过MyLabel观察析构过程原来直接使用QLabel它被销毁时我们不容易从界面上看出具体过程。为了观察这一点可以自己定义一个继承自QLabel的类在析构函数中打印一条日志。先看mylabel.h#ifndefMYLABEL_H#defineMYLABEL_H#includeQLabelclassMyLabel:publicQLabel{public:MyLabel(QWidget*parent);~MyLabel();};#endif// MYLABEL_H再看mylabel.cpp#includemylabel.h#includeiostream#includeQDebugMyLabel::MyLabel(QWidget*parent):QLabel(parent){}MyLabel::~MyLabel(){//std::cout ~MyLabel std::endl;qDebug()MyLabel 销毁;}这个类本身并没有添加复杂功能主要就是在析构函数里多输出一句话。构造函数的QLabel(parent)很重要。MyLabel接收到外面传进来的父对象后还要把它继续交给基类由基类完成相应的初始化。这样new MyLabel(this)传入的窗口对象才真正成为这个标签的父对象。这里的MyLabel没有写Q_OBJECT但仍然可以使用继承来的标签功能和父子对象管理机制。这两项能力并不要求我们给每个派生类都重新写一次Q_OBJECT。程序正常结束时可以在输出区域看到MyLabel 销毁说明标签对象的析构函数确实执行了。所以不是“控件设置了父对象就不需要考虑生命周期了”而是我们可以利用对象树让父对象统一管理适合交给它负责的子对象。4.3qDebug最后再看析构函数中的日志qDebug()MyLabel 销毁;使用它需要包含QDebug。从写法上看它和cout类似都可以通过连续输出内容Qt中很多常用类型也能够直接交给它输出。不过qDebug()使用的是Qt的日志输出机制并不是std::cout的另一个名字。调试Qt程序时通常可以在Qt Creator的应用程序输出区域查看这些信息。这里临时创建的日志流在语句结束时提交消息不需要再写std::endl。QDebug文档日志有什么用呢像这里的析构过程单看窗口消失并不能确认某个对象到底有没有销毁而在析构函数中打印一条信息就能观察程序有没有执行到这个位置。完