2.2 案例2 整理一下目录吧
本案例对应的源代码目录:src/chapter02/ks02_02。程序运行效果见图2-11。
图2-11 案例2运行效果
使用Qt进行开发的目的之一是开发界面类应用。本节将介绍用Qt开发界面类应用的基本步骤,并介绍如何通过修改pro文件的配置使源代码目录保持整洁。
开发界面类项目的过程大概分为四步。
(1)使用Designer绘制对话框资源文件(ui)并保存。
(2)编写界面ui对应的类CDialog。
(3)将相关文件添加到pro。
(4)使用CDialog定义对象。
下面分步骤讲解。
启动Designer,出现如图2-12所示界面,选择template\forms中的Dialog with Buttons Bottom,然后单击【创建】按钮即可完成新建窗体工作。
图2-12 Designer新建窗体
然后,为新建的窗体设置类名:请在窗体空白处单击,然后在属性框中设置对话框的objectName(见图2-13)。设置对话框类名为CDialog,并将UI文件保存为dialog.ui。请记下这两个名称,因为后面会用到。可以根据实际需要对UI文件名、对话框类名进行命名。
图2-13 CDialog属性
然后,从Designer的工具箱的【Display Widgets】选项卡中选择Label控件(类型为QLabel,见图2-14),并拖入窗体。
图2-14 文本控件
双击该Label控件,将文字修改为“This is my dialog!”,如图2-15所示。
图2-15 编辑文本控件内容
然后,单击窗体空白处选中整个窗体,再单击工具栏上的【栅格布局】按钮为窗体设置布局(见图2-16)。
图2-16 对窗体进行栅格布局
2.编写界面ui对应的类CDialog
为dialog.ui编写对应的类CDialog,目的是为对话框增加业务功能。此处的类名CDialog来自图2-13中窗体的objectName。CDialog的头文件dialog.h的代码请见代码清单2-3。
代码清单2-3
// dialog.h #pragma once ① #include <QDialog> namespace Ui { class CDialog; ② } // 基类的名称来自UI文件中对话框的类名:对象查看器中的类名 class CDialog : public QDialog { ③ public: CDialog(QWidget* pParent); ~CDialog(); private: Ui::CDialog* m_pUi; ④ }; |
请注意代码清单2-3中标号①处:#pragma once。该代码的作用是防止编译该头文件时发生重入的情况(多次编译同一个头文件),以免出现编译错误。
在标号②处,对命名空间Ui中的类CDialog做了前置声明,这是因为标号④处要用Ui::CDialog定义指针m_pUi。使用指针和前置声明有2个好处:一是无须在头文件中包含Ui::CDialog的头文件ui_dialog.h,因为在dialog.cpp中将包含该头文件,这在中大型项目中非常重要,因为这样做可以节省很多编译时间;二是当CDialog类(不是Ui::CDialog)需要作为DLL中的类被引出时,不会导致包含dialog.h的其他项目出现编译错误(比如,找不到ui_dialog.h)。
在标号③处,CDialog类的基类是QDialog,这是因为在Designer中绘制UI文件时选择的就是QDialog。如果不清楚CDialog的基类应该选哪一个,可以在Designer的【对象查看器】中查看。如图2-17所示,第一行对象Dialog的类名QDialog就是标号③处的基类名称。
图2-17 对象查看器
在标号④处,为了使用dialog.ui中的布局,需要为类CDialog添加私有成员Ui::CDialog* m_pUi,该对象用来初始化界面。Ui::CDialog来自dialog.ui,是通过Qt的uic命令解析dialog.ui后得到的。在项目生成的临时文件ui_dialog.h中可以找到它的定义,见代码清单2-4。在代码清单2-4中,如标号①处所示,注释中明确指出:对该文件的手工修改都将在重新编译UI文件时被覆盖。在标号②、标号③处,是界面中buttonBox、label这两个控件对象的定义。在标号④处,是初始化界面接口setupUi()的实现,在该接口中完成了对界面的初始化,包括对buttonBox、label这两个控件的构建。在标号⑤处,在命名空间Ui中定义类CDialog,该类(Ui::CDialog)将被用来定义对象m_pUi,见代码清单2-3中标号④处。
代码清单2-4
// ui_dialog.h /******************************************************************************** ** Form generated from reading UI file 'dialog.ui' ** ** Created by: Qt User Interface Compiler version 5.11.1 ** ** WARNING! All changes made in this file will be lost when recompiling UI file! ① ********************************************************************************/ #ifndef UI_DIALOG_H #define UI_DIALOG_H #include <QtCore/QVariant> #include <QtWidgets/QApplication> #include <QtWidgets/QDialog> #include <QtWidgets/QDialogButtonBox> #include <QtWidgets/QLabel> QT_BEGIN_NAMESPACE class Ui_CDialog { public: QDialogButtonBox *buttonBox; ② QLabel *label; ③ void setupUi(QDialog *CDialog) ④ { if (CDialog->objectName().isEmpty()) CDialog->setObjectName(QStringLiteral("CDialog")); CDialog->resize(329, 184); buttonBox = new QDialogButtonBox(CDialog); buttonBox->setObjectName(QStringLiteral("buttonBox")); buttonBox->setGeometry(QRect(80, 120, 161, 32)); buttonBox->setOrientation(Qt::Horizontal); buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); label = new QLabel(CDialog); label->setObjectName(QStringLiteral("label")); label->setGeometry(QRect(100, 40, 171, 16)); retranslateUi(CDialog); QObject::connect(buttonBox, SIGNAL(accepted()), CDialog, SLOT(accept())); QObject::connect(buttonBox, SIGNAL(rejected()), CDialog, SLOT(reject())); QMetaObject::connectSlotsByName(CDialog); } // setupUi void retranslateUi(QDialog *CDialog) { CDialog->setWindowTitle(QApplication::translate("CDialog", "Dialog", nullptr)); label->setText(QApplication::translate("CDialog", "This is my dialog!", nullptr)); } // retranslateUi }; namespace Ui { class CDialog: public Ui_CDialog {}; ⑤ } // namespace Ui QT_END_NAMESPACE #endif // UI_DIALOG_H |
下面给出CDialog的实现文件dialog.cpp,请见代码清单2-5。
代码清单2-5
// dialog.cpp #include "dialog.h" #include "ui_dialog.h" // 头文件名称: dialog.ui -> ui_dialog.h ① CDialog::CDialog(QWidget* pParent) : QDialog(pParent),m_pUi(new Ui::CDialog) { ② m_pUi->setupUi(this); ③ } CDialog::~CDialog() { if (NULL != m_pUi){ ④ delete m_pUi; m_pUi = NULL; } } |
代码清单2-5中,标号①处的代码包含ui_dialog.h头文件,目的是让编译器可以看到Ui::CDialog的定义。这用到了步骤1中保存界面文件时的文件名dialog.ui。Qt的uic命令将dialog.ui文件转换为UI头文件: ui_dialog.h,即前缀ui_加上文件名dailog.ui中的dialog共同拼接成ui_dialog.h。
标号②处,在类CDialog的构造函数的初始化列表中,除了用QDialog(pParent)调用基类的构造函数进行初始化之外,还构建了m_pUi对象。
标号③处,在构造函数中一定要调用m_pUi->setupUi(this),否则界面无法正常显示。因为这处调用就是对界面进行构建。如果对该接口感兴趣,可以单步调试一下这个调用,看看setupUi()内部到底执行了什么操作。
在标号④处,当CDialog析构时需要对m_pUi指向的内存进行释放,并将m_pUi赋值为NULL。
除了将CDialog作为DLL的引出类等特殊需要外,在后续章节中不再限制使用指针还是对象的方式使用Ui::CDilaog。如果使用对象的方式,则需要把dialog.h做两处修改,见代码清单2-6中的标号①处、标号②处,而且dialog.cpp中不再编写#include "ui_dialog.h"的代码。
代码清单2-6
// dialog.h #pragma once #include <QDialog> #include "ui_dialog.h" ① class CDialog : public QDialog { ... private: Ui::CDialog m_ui; ② }; |
3.将相关文件添加到pro
目前已完成的工作包括:设计界面文件dialog.ui、编写CDialog类的定义文件dialog.h和实现文件dialog.cpp。现在把这些文件添加到项目的pro文件,见代码清单2-7。
代码清单2-7
// ks02_02.pro QT += widgets FORMS = dialog.ui HEADERS += ks02_02.pro \ dialog.h SOURCES += main.cpp \ dialog.cpp |
代码清单2-7中,FORMS配置项用来描述项目中用到的UI文件列表;HEADERS和SOURCES两个配置项在前面章节介绍过,只不过在本案例中使用了多个文件。因为用到了界面控件,请确保编写QT+=widgets,否则将导致程序构建失败。
4.使用CDialog定义对象
如果用类CDialog定义对象,首先需要包含CDialog的头文件(dialog.h),然后才能定义CDialog的对象并调用其接口,见代码清单2-8。
代码清单2-8
// main.cpp #include <QApplication> #include <iostream> #include "qglobal.h" #include "dialog.h" using std::cout; using std::endl; int main(int argc, char * argv[]){ Q_UNUSED(argc); Q_UNUSED(argv); QApplication app(argc, argv); CDialog dlg(NULL); dlg.exec(); return 0; } |
到现在为止,为项目添加界面的工作就结束了,可以构建项目了。
但是当项目构建完成后,看一下源代码目录就会发现临时文件和临时目录太多了(如图图2-18所示方框内的文件或目录),简直杂乱不堪。一般情况下,项目组会使用代码管理工具(比如SVN)管理代码,而且可以预先设置代码入库的过滤条件,因此在提交代码时临时文件或目录不会被入库。但是,当需要备份一下源代码目录并打包时,如果有这么多临时文件(有的临时文件尺寸比较大),那可太不方便了。下面介绍如何通过修改pro文件的配置来整理源代码目录。
图2-18 案例2源代码目录
整理源代码目录的方法是引入环境变量,然后通过环境变量来设置pro中的各种路径。
首先引入一个TRAINDEVHOME的环境变量,它指向项目src的上级目录。代码的目录结构如下。
TRAINDEVHOME
------bin
------obj
------src
其中bin 、obj、src都是TRAINDEVHOME的子目录。bin目录用来存放项目生成的可执行程序和动态链接库,obj是构建项目时生成的临时文件的根目录
在pro文件中使用环境变量的语法:$$(环境变量)。比如:$$(TRAINDEVHOME)。对pro的修改见代码清单2-9。
代码清单2-9
# ks02_02.pro ... OBJECTS_DIR = $$(TRAINDEVHOME)/obj/chapter02/ks02_02 DESTDIR = $$(TRAINDEVHOME)/bin MOC_DIR = $$OBJECTS_DIR/moc UI_DIR = $$OBJECTS_DIR/ui |
在代码清单2-9中,OBJECTS_DIR是Qt的关键字,用来表示项目生成的临时文件的存放目录。将OBJECTS_DIR设置到obj下对应本案例的子目录。
DESTDIR是Qt的关键字,用来表示目标文件存放目录,即项目最终生成的EXE或DLL的存放目录。如果是EXE,将其设置到TRAINDEVHOME的bin子目录;如果是DLL,则设置到lib子目录。
MOC_DIR、UI_DIR是Qt的关键字,用来表示Qt的moc和uic命令生成的临时文件存放目录。将它们分别设置到OBJECTS_DIR下面的moc子目录和ui子目录。
除此之外,如果使用Qt Creator进行开发,还需要设置默认构建路径,否则会导致影子构建时生成的临时文件被放到源代码目录中。选择Qt Creator的【工具】|【选项】菜单项,出现【选项】对话框,选择【构建和运行】标签,再选择【概要】标签,最后修改Default build directory(见图2-19),将其值修改为(关于该值的拼写请见本书配套资源中【Qt Creator 4.7.2的Default build directory设置】):
%{CurrentProject:VcsTopLevelPath}/obj/%{CurrentBuild:Name}/%{JS: Util.asciify("build-%{CurrentProject:Name}-%{CurrentKit:FileSystemName}")}
图2-19 Qt Creator默认构建目录
修改pro文件并设置完Qt Creator后,再次构建项目后得到的源代码目录见图2-20。
图2-20 整理后的目录
如果使用VS 2017命令行进行构建,在每次修改pro后请重新执行qmake以便更新相应的Makefile文件;如果用VS 2017的IDE进行构建,则需要更新VS 2017的IDE中的项目文件(后缀为.vcxproj的文件),方法是执行qmake –tp vc,然后用VS 2017 的IDE重新加载项目文件。
本案例介绍了开发界面类应用的步骤以及通过修改pro配置来整理源代码目录的方法,现在汇总一下知识点:
(1)如果项目中需要使用界面,那么在pro中请务必添加:
QT +=widgets |
(2)CDialog类的头文件中,请注意CDialog基类名称的来源以及私有的指针成员变量m_pUi以及对于Ui::CDialog的前置声明,见代码清单2-10。
代码清单2-10
namespace Ui { class CDialog; } class CDialog : public QDialog { ... private: Ui::CDialog* m_pUi; }; |
(3)请注意dialog.ui对应头文件ui_dialog.h的文件名的构成规则。
(4)CDialog类的构造函数中一定要调用m_pUi->setupUi(this)。
(5)要在pro中通过环境变量设置相关目录。环境变量在使用时应该用下列语法:
$$(环境变量)
(6)可以在pro中定义变量,变量在使用时应该用下列语法:
$$变量名称
(7)另外,还介绍了Qt关于路径设置的关键字。
- DESTDIR:存放最终生成的目标文件的路径。
- OBJECTS_DIR:本项目临时文件的存放目录。
- MOC_DIR:用来存放qt的moc命令生成的临时文件。
- UI_DIR:用来存放qt的uic命令生成的临时文件。当项目中包含UI文件时需要用到该目录。
- FORMS:用来描述项目中包含的UI文件列表。