當我們准備好 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,開始真正的代碼閱讀。
|
1
|
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 文件創建出來多個虛擬目錄節點。這樣的項目結構看起來會清晰很多。
|
1
2
3
4
5
|
#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)。有關$$以及相關運算符的使用相當重要。
$$運算符通常用於展開變量的內容,展開的內容可以用於變量的賦值,也可以用於函數的傳參。例如:
|
1
2
3
|
EVERYTHING = $$SOURCES $$HEADERS
message("The project contains the following files:")
message($$EVERYTHING)
|
上面的代碼中,第一行將SOURCES和HEADERS的內容賦值給EVERYTHING;第三行則將EVERYTHING作為函數參數賦值給message()函數。如果沒有$$運算符,將只會輸出EVERYTHING字符串。
變量可以保存環境變量。這些變量可以在 qmake 執行時計算出,或者直接包含在 Makefile 中以便構建時使用。如果需要在 qmake 運行時獲取環境變量的值,使用$$()或$${}運算符。例如:
|
1
2
|
DESTDIR = $$(PWD)
message(The project will be installed in $$DESTDIR)
|
在上面代碼中,PWD是 qmake 內置的一個環境變量,用於表示當前正在處理的文件所在文件夾的絕對路徑。使用$$()或${}運算符,會在 qmake 運行時將值賦給DESTDIR。如果需要在生成 Makefile 時獲取環境變量的值,則需要使用$()運算符。例如:
|
1
2
3
4
5
6
|
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 運行時進行變量展開。
下面再來看另外的代碼:
|
1
2
|
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 是這樣做的:
|
1
2
3
|
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 目錄。+=運算符就像它所展示的那樣,用於追加新的值。copydata和BUILD_TESTS都是在 qtcreator.pri 中定義的宏。因為我們是在最前面include了 qtcreator.pri,所以我們可以自由使用在 qtcreator.pri 文件中定義的變量。類似!isEmpty(BUILD_TESTS):SUBDIRS += tests這樣的寫法是一種簡寫,完整的寫法應該如下所示:
|
1
2
3
|
!isEmpty(BUILD_TESTS) {
SUBDIRS += tests
}
|
有關isEmpty()這樣的函數,我們會在下面詳細介紹。我們在看這段代碼時,可以同 C++ 代碼作類比,以便我們理解:
|
1
2
3
4
5
6
7
8
9
10
11
|
SUBDIRS = src share
if (unix) {
if (!macx) {
if (!isEmpty(copydata)) {
SUBDIRS += bin
}
}
}
if (!isEmpty(BUILD_TESTS)) {
SUBDIRS += tests
}
|
接下來我們遇到的是
|
1
2
3
4
|
DISTFILES += dist/copyright_template.txt \
README.md \
$$files(dist/changes-*) \
...
|
DESTFILES知道需要在最終的目標包括的文件。按照 qmake 的文檔,這一特性只適用於 UnixMake。這里我們又遇到了熟悉的$$file(),只不過這里不是變量展開,而是函數調用。
qmake 提供了兩類函數:替換函數(replace functions)和測試函數(test fucntion)。替換函數用於處理數據並將處理結果返回;測試函數的返回值只能是bool值,並且可以用於一些測試的情形。在使用時,替換函數需要添加$$先導符而測試函數則不需要。
$$file()正是一個替換函數,接受一個正則表達式作為參數,其返回值是所有符合這個正則表達式的文件名列表。因此,$$file(dist/changes-*)返回的是在當前目錄下的 dist 文件夾中,所有以 changes- 開頭的文件,將它們全部添加到了DESTFILES。另外,這一函數還可以有第二個參數,是一個bool值,默認是false,表示是不是要遞歸尋找文件。
之后我們看到了
|
1
2
3
|
exists(src/shared/qbs/qbs.pro) {
...
}
|
exists()則是一個測試函數,顧名思義,該函數用於測試其參數作為文件名,所代表的文件是否存在。注意測試函數的使用:它可以直接作為測試條件,后面跟着一對大括號,如果函數返回值為true則執行塊中的語句。這里我們發現 src/shared/qbs/qbs.pro 並不存在,因此其中的語句並不會執行。
下面是語句
|
1
2
|
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並沒有使用$$運算符。因為在使用該函數時,第一個參數是變量名,函數會自己取該變量名的實際值。
|
1
2
3
4
|
macx: PLATFORM = "mac"
else:win32: PLATFORM = "windows"
else:linux-*: PLATFORM = "linux-$${ARCHITECTURE}"
else: PLATFORM = "unknown"
|
定義了一個新的宏PLATFORM。注意這里使用了前面剛剛定義的ARCHITECTURE宏。
接下來,
|
1
2
|
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 文件中,很多地方都使用了類似的寫法。
跳過部分代碼,接下來是一大段:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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 內置了很多屬性值,例如:
|
1
2
3
4
5
6
7
8
9
10
11
12
|
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 可以自定義屬性:
|
1
|
qmake -set PROPERTY VALUE
|
然后,我們就可以用下面語句獲取這個屬性:
|
1
|
qmake -query PROPERTY
|
在 pro 文件中,則可以使用$$[]獲取這些屬性。可以查閱文檔找到 qmake 內置了哪些屬性。
語句
|
1
|
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 運行完畢后,會接着執行這些目標,直到編譯成功。
例如,
|
1
2
3
4
5
|
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:
|
1
|
QMAKE_EXTRA_TARGETS += mytarget mytarget2
|
最后,我們來看
|
1
2
3
4
5
6
|
win32 {
deployqt.commands ~= s,/,\\\\,g
bindist.commands ~= s,/,\\\\,g
bindist_installer.commands ~= s,/,\\\\,g
installer.commands ~= s,/,\\\\,g
}
|
有是一個新的語法~=。~=運算符將符合正則表達式的內容替換為后面的部分。例如DEFINES ~= s/QT_[DT].+/QT會將以QT_D或QT_T開頭的文本替換為QT。后面s,/,\\\\,g是替換操作。三個逗號分為四個部分:第一個s表示輸入字符串;第二個/表示 /;第三個\\\\表示 \,之所以是四個,是因為 \ 需要轉義;第四個g表示全局替換。合起來的意思就是,將輸入字符串中的 / 全部替換為 \。這是適配命令路徑中,Unix 的 / 和 Windows 的 \。這一個技巧在編寫跨平台代碼中非常有用,很多時候我們在 pro 中給出了 Unix 格式的路徑,只需要使用簡單的語句,例如
|
1
2
|
PWD_WIN = $${PWD}
PWD_WIN ~= s,/,\\,g
|
就可以轉換為合法的 Windows 路徑。
本章我們着重學習了 Qt Creator 的主項目文件 qtcreator.pro 的寫法。下一節我們將詳細介紹 qtcreator.pri 的寫法。
https://www.devbean.net/2016/08/qt-creator-source-study-03/

