首页 Qt Creator 源码学习 Qt Creator 源码学习 03:qtcreator.pro

Qt Creator 源码学习 03:qtcreator.pro

7 2K

当我们准备好 Qt Creator 的源代码之后,首先进入到它的目录,来看一下它的源代码目录有什么奥秘。

Qt Creator 代码目录结构

这里一共有 9 个文件夹和 9 个文件。我们来一一看看它们都是干什么用的。

  • .git: 版本控制 git 的隐藏目录,这与 Qt Creator 代码没有关系。
  • bin: 生成 Linux 平台 shell 脚本。
  • dist: 安装文件配置信息和版本更新记录。
  • doc: 生成 doxygen 文档的配置文件。
  • qbs: QBS 配置文件。QBS,即 Qt Build Suite,是一种跨平台的编译工具,目的是将高层的项目描述(使用类似 QML 的语言)转换成底层的编译描述(供 make 等工具使用的信息)。它可以简化多平台的编译过程。QBS 与 qmake 类似,区别在于前者适用于任意项目,而后者一般仅供 Qt 项目使用。我们在阅读代码时将关注 qmake,不会深入研究 QBS 的使用。
  • scripts: Qt Creator 使用的 perl 以及 python 等脚本。
  • share: 源代码中所需要的一些非代码共享文件,例如代码模板等。
  • src: Qt Creator 源代码文件。
  • tests: Qt Creator 测试代码。
  • .gitignore: git 忽略文件配置。
  • .gitmodules: git 子模块配置。
  • HACKING: Qt Creator 编码规范。
  • LICENSE.GPL3-EXCEPT: GPLv3 协议。
  • qtcreator.pri: Qt Creator 项目需要使用的通用配置,该文件一般会被 include 到大部分 pro 文件。
  • qtcreator.pro: Qt Creator 的 qmake 项目文件。
  • qtcreator.qbs: Qt Creator 的 QBS 项目文件。
  • qtcreatordata.pri: Qt Creator 数据相关的配置。
  • README.md: 有关如何编译 Qt Creator 等相关事宜的一些说明。

阅读源代码,一般可以从main()着手。但是阅读 Qt 项目的源代码,我们也可以从 pro 文件开始。pro 文件是 Qt 项目组织结构,规定了我们希望该项目如何编译、编译之后要做什么操作等。

下面我们从根目录的 qtcreator.pro 开始。使用 Qt Creator 或者任意文本编辑器打开 qtcreator.pro,开始真正的代码阅读。

include(qtcreator.pri)

第一行是include qtcreator.pri。前面我们提到过,qtcreator.pri 中定义了很多函数和适用于各个模块的通用操作。pri 文件可以理解为 pro 文件片段,可以使用include操作符将其引入一个 pro 文件。qmake 会自动处理引用操作,类似于将 pri 文件的全部内容复制到include语句处。这与 C++ 的#include指令类似。这里的处理是线性的,也就是 qmake 会从上向下进行解析。因此,如果你在 pri 中定义了一个函数,那么必须在include语句之后才能正常使用该函数。这是在使用时需要注意的。有关 qtcreator.pri 文件的内容,会在以后的文章中详细介绍。如果你使用 Qt Creator 打开,include语句会在左侧的项目树中显示一个节点。这种节点不需要物理上的文件夹隔离,只需要include不同的 pri 文件即可。这样,即便你的所有文件都在同一个目录下,你也可以使用 pri 文件创建出来多个虚拟目录节点。这样的项目结构看起来会清晰很多。

#version check qt
!minQtVersion(5, 6, 0) {
    message("Cannot build Qt Creator with Qt version ${QT_VERSION}.")
    error("Use at least Qt 5.6.0.")
}

接下来的几行用于判断 Qt 的版本。minQtVersion()是在 qtcreator.pri 中定义的函数。没错!pro 也可以定义自己的函数!这正是 pro 的强大之处。我们会在后面详细介绍如何定义函数。顾名思义,这个函数函数用于判断 Qt 的版本。前面的!即取非运算符,这与 C++ 一致。当 Qt 的版本低于 5.6.0 时,执行块中的操作。message()是 qmake 预定义的函数,类似于qDebug(),可以在控制台输出一段文本。这里我们输出的是“Cannot build Qt Creator with Qt version $${QT_VERSION}.”。字符串最后的$${QT_VERSION}是占位符,会使用QT_VERSION变量的内容进行替换。这一操作被称为变量展开(variable expansion)。有关$$以及相关运算符的使用相当重要。

$$运算符通常用于展开变量的内容,展开的内容可以用于变量的赋值,也可以用于函数的传参。例如:

EVERYTHING = $SOURCES $HEADERS
message("The project contains the following files:")
message($EVERYTHING)

上面的代码中,第一行将SOURCESHEADERS的内容赋值给EVERYTHING;第三行则将EVERYTHING作为函数参数赋值给message()函数。如果没有$$运算符,将只会输出EVERYTHING字符串。

变量可以保存环境变量。这些变量可以在 qmake 执行时计算出,或者直接包含在 Makefile 中以便构建时使用。如果需要在 qmake 运行时获取环境变量的值,使用$$()$${}运算符。例如:

DESTDIR = $(PWD)
message(The project will be installed in $DESTDIR)

在上面代码中,PWD是 qmake 内置的一个环境变量,用于表示当前正在处理的文件所在文件夹的绝对路径。使用$$()${}运算符,会在 qmake 运行时将值赋给DESTDIR。如果需要在生成 Makefile 时获取环境变量的值,则需要使用$()运算符。例如:

DESTDIR = $(PWD)
message(The project will be installed in $DESTDIR)

DESTDIR = $(PWD)
message(The project will be installed in the value of PWD)
message(when the Makefile is processed.)

在上面的语句中,PWD的值在 qmake 处理是就已经获取到了,但是$(PWD)则会在生成的 Makefile 中赋值给DESTDIR。这能够保证在处理 Makefile 时环境变量是正确的。

通过上面的解释,我们知道,$${QT_VERSION}会在 qmake 运行时进行变量展开。

下面再来看另外的代码:

TEMPLATE = subdirs
CONFIG += ordered

这是 qmake 典型的配置。TEMPLATE即代码模板,将告诉 qmake 我们要怎么生成最后的文件。它的可选值分别是:

  • app:创建用于构建可执行文件的 Makefile。
  • lib:创建用于构建库的 Makefile。
  • subdirs:创建依次构建子目录中文件的 Makefile。子目录使用SUBDIRS变量指定。
  • aux:创建不构建任何东西的 Makefile。如果构建目标不需要编译器,就可以使用这个模板。例如,你的项目使用的是解释型语言,就可以这么做。注意,此时生成的 Makefile 仅适用于基于 Makefile 的生成器,不一定能供 vcxproj 或 Xcode 使用。
  • vcapp:仅适用于 Windows 平台,用于生成 VS 应用程序项目。
  • vclib:仅适用于 Windows 平台,用于生成 VS 库项目。

我们最常用的是前三种设置。对于大型项目,一般会分成多个源代码文件夹,因此,Qt Creator 使用的是 subdirs。接下来一行,CONFIG += ordered意思是,按照SUBDIRS书写顺序来编译。很多时候,我们虽然将源代码分为不同目录,但是这些目录之间是存在依赖关系的。比如,一个基础类库要被其它所有模块使用,在编译时,该类库应该首先被编译。这要求我们按照一定的顺序来添加SUBDIRS。有关这一点,Qt Creator 是这样做的:

SUBDIRS = src share
unix:!macx:!isEmpty(copydata):SUBDIRS += bin
!isEmpty(BUILD_TESTS):SUBDIRS += tests

首先,SUBDIRS只有两个目录:src 和 share。按照顺序,应该是先编译 src,然后编译 share。后面则是一串复杂的判断:对于 Unix 平台(unix),如果不是 Mac OS(!macx),并且copydata不为空(!isEmpty(copydata)),则需要再增加一个 bin 目录。最后再判断,如果BUILD_TESTS不为空(!isEmpty(BUILD_TESTS)),则再增加一个 tests 目录。+=运算符就像它所展示的那样,用于追加新的值。copydataBUILD_TESTS都是在 qtcreator.pri 中定义的宏。因为我们是在最前面include了 qtcreator.pri,所以我们可以自由使用在 qtcreator.pri 文件中定义的变量。类似!isEmpty(BUILD_TESTS):SUBDIRS += tests这样的写法是一种简写,完整的写法应该如下所示:

!isEmpty(BUILD_TESTS) {
    SUBDIRS += tests
}

有关isEmpty()这样的函数,我们会在下面详细介绍。我们在看这段代码时,可以同 C++ 代码作类比,以便我们理解:

SUBDIRS = src share
if (unix) {
    if (!macx) {
        if (!isEmpty(copydata)) {
            SUBDIRS += bin
        }
    }
}
if (!isEmpty(BUILD_TESTS)) {
    SUBDIRS += tests
}

接下来我们遇到的是

DISTFILES += dist/copyright_template.txt \
             README.md \
             $files(dist/changes-*) \
             ...

DISTFILES知道需要在最终的目标包括的文件。按照 qmake 的文档,这一特性只适用于 UnixMake。这里我们又遇到了熟悉的$$file(),只不过这里不是变量展开,而是函数调用。

qmake 提供了两类函数:替换函数(replace functions)和测试函数(test fucntion)。替换函数用于处理数据并将处理结果返回;测试函数的返回值只能是bool值,并且可以用于一些测试的情形。在使用时,替换函数需要添加$$先导符而测试函数则不需要。

$$file()正是一个替换函数,接受一个正则表达式作为参数,其返回值是所有符合这个正则表达式的文件名列表。因此,$$file(dist/changes-*)返回的是在当前目录下的 dist 文件夹中,所有以 changes- 开头的文件,将它们全部添加到了DISTFILES。另外,这一函数还可以有第二个参数,是一个bool值,默认是false,表示是不是要递归寻找文件。

之后我们看到了

exists(src/shared/qbs/qbs.pro) {
    ...
}

exists()则是一个测试函数,顾名思义,该函数用于测试其参数作为文件名,所代表的文件是否存在。注意测试函数的使用:它可以直接作为测试条件,后面跟着一对大括号,如果函数返回值为true则执行块中的语句。这里我们发现 src/shared/qbs/qbs.pro 并不存在,因此其中的语句并不会执行。

下面是语句

contains(QT_ARCH, i386): ARCHITECTURE = x86
else: ARCHITECTURE = $QT_ARCH

QT_VERSION是 qmake 内置的一个变量,用于表示 Qt 的架构。很明显,contains()是一个测试函数,其函数原型是contains(variablename, value),当变量variablename中包含了value时,测试通过。那么,上面语句即是,如果QT_ARCH中有i386,则将ARCHITECTURE赋值为x86,否则就是$$QT_ARCH。注意在使用contains函数时,QT_ARCH并没有使用$$运算符。因为在使用该函数时,第一个参数是变量名,函数会自己取该变量名的实际值。

macx: PLATFORM = "mac"
else:win32: PLATFORM = "windows"
else:linux-*: PLATFORM = "linux-${ARCHITECTURE}"
else: PLATFORM = "unknown"

定义了一个新的宏PLATFORM。注意这里使用了前面刚刚定义的ARCHITECTURE宏。

接下来,

BASENAME = $(INSTALL_BASENAME)
isEmpty(BASENAME): BASENAME = qt-creator-${PLATFORM}$(INSTALL_EDITION)-${QTCREATOR_VERSION}$(INSTALL_POSTFIX)

是一种常见的写法。首先,我们定义了BASENAME宏为$$(INSTALL_BASENAME);之后,如果BASENAME为空的话(使用了测试函数isEmpty()进行判断),则定义新的BASENAME的值。这种写法一方面允许我们在编译时通过传入自定义值改变默认设置(也就是说,如果之前定义了INSTALL_BASENAME,那么就会使用我们定义的值),否则就会生成一个默认值。以后我们会发现,Qt Creator 的 pro 文件中,很多地方都使用了类似的写法。

跳过部分代码,接下来是一大段:

macx {
    APPBUNDLE = "$OUT_PWD/bin/Qt Creator.app"
    BINDIST_SOURCE = "$OUT_PWD/bin/Qt Creator.app"
    BINDIST_INSTALLER_SOURCE = $BINDIST_SOURCE
    deployqt.commands = $PWD/scripts/deployqtHelper_mac.sh \"${APPBUNDLE}\" \"$[QT_INSTALL_TRANSLATIONS]\" \"$[QT_INSTALL_PLUGINS]\" \"$[QT_INSTALL_IMPORTS]\" \"$[QT_INSTALL_QML]\"
    codesign.commands = codesign --deep -s \"$(SIGNING_IDENTITY)\" $(SIGNING_FLAGS) \"${APPBUNDLE}\"
    dmg.commands = $PWD/scripts/makedmg.sh $OUT_PWD/bin ${BASENAME}.dmg
    #dmg.depends = deployqt
    QMAKE_EXTRA_TARGETS += codesign dmg
} else {
    BINDIST_SOURCE = "$(INSTALL_ROOT)$QTC_PREFIX"
    BINDIST_INSTALLER_SOURCE = "$BINDIST_SOURCE/*"
    deployqt.commands = python -u $PWD/scripts/deployqt.py -i \"$(INSTALL_ROOT)$QTC_PREFIX\" \"$(QMAKE)\"
    deployqt.depends = install
    win32 {
        deployartifacts.depends = install
        deployartifacts.commands = git clone "git://code.qt.io/qt-creator/binary-artifacts.git" -b $BINARY_ARTIFACTS_BRANCH&& xcopy /s /q /y /i "binary-artifacts\\win32" \"$(INSTALL_ROOT)$QTC_PREFIX\"&& rmdir /s /q binary-artifacts
        QMAKE_EXTRA_TARGETS += deployartifacts
    }
}

这里使用macx分为两部分。很明显,如果系统是macx,则定义宏APPBUNDLE。我们需要详细解释的是,在定义新的变量时,Qt Creator 所用到的那些宏。首先,$$OUT_PWD是 qmake 生成的 Makefile 所在的文件夹。

下面我们会看到一个新的语法:$$[]。这是取 qmake 的属性。qmake 内置了很多属性值,例如:

message(Qt version: $[QT_VERSION])
message(Qt is installed in $[QT_INSTALL_PREFIX])
message(Qt resources can be found in the following locations:)
message(Documentation: $[QT_INSTALL_DOCS])
message(Header files: $[QT_INSTALL_HEADERS])
message(Libraries: $[QT_INSTALL_LIBS])
message(Binary files (executables): $[QT_INSTALL_BINS])
message(Plugins: $[QT_INSTALL_PLUGINS])
message(Data files: $[QT_INSTALL_DATA])
message(Translation files: $[QT_INSTALL_TRANSLATIONS])
message(Settings: $[QT_INSTALL_CONFIGURATION])
message(Examples: $[QT_INSTALL_EXAMPLES])

在运行时,qmake 可以自定义属性:

qmake -set PROPERTY VALUE

然后,我们就可以用下面语句获取这个属性:

qmake -query PROPERTY

在 pro 文件中,则可以使用$$[]获取这些属性。可以查阅文档找到 qmake 内置了哪些属性。

语句

deployqt.commands = $PWD/scripts/deployqtHelper_mac.sh \"${APPBUNDLE}\" \"$[QT_INSTALL_TRANSLATIONS]\" \"$[QT_INSTALL_PLUGINS]\" \"$[QT_INSTALL_IMPORTS]\" \"$[QT_INSTALL_QML]\"

定义了一个目标 deployqt,这个目标的命令是$$PWD/scripts/deployqtHelper_mac.sh \"$${APPBUNDLE}\" \"$$[QT_INSTALL_TRANSLATIONS]\" \"$$[QT_INSTALL_PLUGINS]\" \"$$[QT_INSTALL_IMPORTS]\" \"$$[QT_INSTALL_QML]\"。我们可以使用message()函数输出这条命令。命令的具体实现暂不深究,感兴趣的话可以阅读 scripts/deployqtHelper_mac.sh 文件。接下来的语句是类似的。最后,这些定义的目标被添加到QMAKE_EXTRA_TARGETS。这才是真正重要的内容。

尽管 qmake 努力成为一个跨平台的构建工具,但是很多时候,我们不得不使用特定平台的语句。例如,一个常见的任务是,在编译完成之后,将预置的配置文件复制到特定目录。这种目标可以通过类似的语法进行定义,然后将定义好的目标添加到QMAKE_EXTRA_TARGETS。当 qmake 运行完毕后,会接着执行这些目标,直到编译成功。

例如,

mytarget.target = .buildfile
mytarget.commands = touch $mytarget.target
mytarget.depends = mytarget2

mytarget2.commands = @echo Building $mytarget.target

mytarget是一个自定义目标;mytarget.target是这个自定义目标的名字。之后生成的 Makefile 中将会使用这个名字作为 target。mytarget.commands定义了这个目标的命令:使用touch命令生成一个文件。mytarget.depends定义这个目标依赖于mytarget2,尽管mytarget2是在后面定义的。最后,我们将这两个目标都添加到QMAKE_EXTRA_TARGETS

QMAKE_EXTRA_TARGETS += codesign dmg

最后,我们来看

win32 {
    deployqt.commands ~= s,/,\\\\,g
    bindist.commands ~= s,/,\\\\,g
    bindist_installer.commands ~= s,/,\\\\,g
    installer.commands ~= s,/,\\\\,g
}

有是一个新的语法~=~=运算符将符合正则表达式的内容替换为后面的部分。例如DEFINES ~= s/QT_[DT].+/QT会将以QT_DQT_T开头的文本替换为QT。后面s,/,\\\\,g是替换操作。三个逗号分为四个部分:第一个s表示输入字符串;第二个/表示 /;第三个\\\\表示 \,之所以是四个,是因为 \ 需要转义;第四个g表示全局替换。合起来的意思就是,将输入字符串中的 / 全部替换为 \。这是适配命令路径中,Unix 的 / 和 Windows 的 \。这一个技巧在编写跨平台代码中非常有用,很多时候我们在 pro 中给出了 Unix 格式的路径,只需要使用简单的语句,例如

PWD_WIN = ${PWD}
PWD_WIN ~= s,/,\\,g

就可以转换为合法的 Windows 路径。

本章我们着重学习了 Qt Creator 的主项目文件 qtcreator.pro 的写法。下一节我们将详细介绍 qtcreator.pri 的写法。

7 评论

soga_zh 2016年8月18日 - 21:26

豆子老师,我想用QT creator制作一个界面可以调用函数,该怎么做?有没有类似的例子啊?

回复
豆子 2016年8月19日 - 09:28

什么是“制作一个界面可以调用函数”?

回复
soga_zh 2016年8月19日 - 09:30

就是制作一个界面,利用其中的一个按钮来调用C++函数

回复
gruLewis 2016年9月12日 - 23:10

请问下有没有方法能够直接在pro文件中调用脚本?然后在Qtcreator编译的时候执行脚本。
我想完成一个复制文件到某个路径下的动作。
貌似QMAKE_EXTRA_TARGETS 不能一步到位。请指教。

回复
豆子 2016年9月13日 - 10:23

复杂的脚本暂时没有研究,不过复制文件这种是支持的。细节可以参考 qtcreatordata.pri 文件,注意看里面的 $$QMAKE_COPY 命令,这是 qmake 的跨平台宏,在 Windows 上会编译成 copy,Linux 上就是 cp,诸如此类。qmake 定义了一系列类似的命令,定义是在 Qt 安装目录的 mkspecs\common\shell-unix.conf 或者 mkspecs\common\shell-win32.conf 这样的文件中。

回复
yw66 2019年5月6日 - 10:02

DESTFILES知道需要在最终的目标包括的文件。按照 qmake..........
“DESTFILES”写错了,应该是“DISTFILES”

回复
豆子 2019年5月8日 - 15:14

多谢指出!

回复

回复 豆子 取消回复

关于我

devbean

devbean

豆子,生于山东,定居南京。毕业于山东大学软件工程专业。软件工程师,主要关注于 Qt、Angular 等界面技术。

主题 Salodad 由 PenciDesign 提供 | 静态文件存储由又拍云存储提供 | 苏ICP备13027999号-2