Qt Creator 原始碼學習筆記03,大型專案如何管理工程

kevinlq發表於2021-11-29

閱讀本文大概需要 6 分鐘

一個專案隨著功能開發越來越多,專案必然越來越大,工程管理成本也越來越高,後期維護成本更高。如何更好的組織管理工程,是非常重要的

今天我們來學習下 Qt Creator 是如何組織管理這麼龐大的一個專案工程的

QMake 多工程管理方法

我們知道 Qt 採用 qmake語法進行組織管理工程結構,想要更好的學習管理一個工程需要你瞭解基本的qmake語法

Qt當中,一般以xx.pro結尾的檔案是某個工程檔案,我們只要開啟該檔案即可開啟該檔案管理的工程

單工程基本用法

比如我們新建一個MainWindow工程,那麼自動會生成如下結構的工程檔案

其中untitled.pro檔案內容如下所示

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

CONFIG += c++11
TARGET = TestDemo
TEMPLATE = app

SOURCES += \
    main.cpp \
    mainwindow.cpp

HEADERS += \
    mainwindow.h

FORMS += \
    mainwindow.ui

該檔案描述了這個工程一些基本資訊

  • QT += 表示需要包含哪些模組
  • greaterThan 可以判斷 Qt 的一些版本進而做一些版本之間差異的事情
  • TEMPLATE 表示該工程編譯完最終會連線生成一個app,即會生成xx.exe可執行檔案
  • TARGET 表示該工程最終生成的檔名字,如果沒有配置預設取該工程名字

那麼如果我們想編譯生成一個動態庫或者靜態庫該怎麼辦?關鍵語句TEMPLATE來進行控制

TEMPLATE = lib

此時編譯該工程預設會生成動態庫

TEMPLATE = lib

CONFIG += staticlib

此時編譯該工程又會生成靜態庫,所以關鍵地方就在上面兩句配置

多工程用法

多工程專案中一般是某些核心模組編譯成庫(靜態庫、動態庫),然後在依賴的地方進行引入即可

比如我們有一個字串處理工具模組StringUtil,該模組最終編譯完會生成一個動態庫StringUtil.dll,然後我們其中一個模組需要使用到該模組,那麼該工程怎麼使用呢?

首先是工程pro檔案需要匯入該動態庫,這樣才能載入進來參與編譯,否則會提示某些函式未定義的錯誤

LIBS    += -L$$PWD/../../ -lStringUtil

外掛依賴管理緣由

上述程式碼正常情況下是沒有任何問題的,但是,但是,但是

凡是重要的事情肯定要說三遍,你以為這樣寫就完事了,那麼說明你的這個庫被依賴的工程比較少,如果這個基礎庫在 n 個工程下面要使用呢?

到這裡相比有些人就說了,哪個工程要使用直接複製上述程式碼過去不就行了,這樣做從功能上來看是沒有問題,可以正常使用和執行,但是你想過沒有,未來的那一天因為特殊原因這個庫有變化(名字、路徑等等),你是不是得修改這 n 個地方使用該庫的地方呀

程式設計師碰見重複的程式碼肯定是不可以忍受的,肯定要想辦法封裝一下,將修改減少到最小,那麼怎麼實現比較好呢?

核心思想就是把依賴匯入相關流程使用迴圈搞定,工程初始化時(也就是qmake)自動根據某個規則載入你的所有依賴即可

管理方案

當你閱讀 Qt Creator 原始碼的時候就可以看到比較有意思的寫法,每個外掛或者動態庫都有自己的依賴配置pri檔案,該檔案記錄了這個庫或外掛依賴那些庫、那些外掛

每個外掛的依賴配置檔案命名類似這樣:xxx_dependencies.pri,我們拿歡迎介面外掛來舉個例子分析下

開啟該檔案 welcome_dependencies.pri。,檢視檔案內容

# 外掛名字
QTC_PLUGIN_NAME = Welcome

# 外掛依賴的庫
QTC_LIB_DEPENDS += \
    extensionsystem \
    utils
    
# 外掛依賴的外掛
QTC_PLUGIN_DEPENDS += \
    coreplugin
  • QTC_PLUGIN_NAME 表示了當前生成動態庫或者外掛的名字
  • QTC_LIB_DEPENDS 表示當前庫依賴的庫檔名稱,多個庫依次追加即可
  • QTC_PLUGIN_DEPENDS 表示當前外掛依賴的外掛名稱,比如welcome外掛依賴核心Coreplugin外掛

那麼這些定義的檔案是怎麼載入進來的?又是怎麼起作用的呢?原始碼面前了無祕密,我們開啟qtcreator.pri檔案來一探究竟,重點關注 244 行到 277 行之間的內容,可以看到如下內容:

!isEmpty(QTC_PLUGIN_DEPENDS) {
    LIBS *= -L$$IDE_PLUGIN_PATH  # plugin path from output directory
    LIBS *= -L$$LINK_PLUGIN_PATH  # when output path is different from Qt Creator build directory
}

# recursively resolve plugin deps
done_plugins =
for(ever) {
    isEmpty(QTC_PLUGIN_DEPENDS): \
        break()
    done_plugins += $$QTC_PLUGIN_DEPENDS
    for(dep, QTC_PLUGIN_DEPENDS) {
        dependencies_file =
        for(dir, QTC_PLUGIN_DIRS) {
            exists($$dir/$$dep/$${dep}_dependencies.pri) {
                dependencies_file = $$dir/$$dep/$${dep}_dependencies.pri
                break()
            }
        }
        isEmpty(dependencies_file): \
            error("Plugin dependency $$dep not found")
        include($$dependencies_file)
        LIBS += -l$$qtLibraryName($$QTC_PLUGIN_NAME)
    }
    QTC_PLUGIN_DEPENDS = $$unique(QTC_PLUGIN_DEPENDS)
    QTC_PLUGIN_DEPENDS -= $$unique(done_plugins)
}

上述程式碼核心思想就是迴圈獲取依賴庫檔案,然後進行引入

第一句 for(ever)是一個無限迴圈,相當於是死迴圈while(1),等到 break 語句才會退出

第二個迴圈獲取$$QTC_PLUGIN_DEPENDS值挨個進行遍歷,這個迴圈是為了檢測引入每個依賴外掛

第三個迴圈,首先會判斷對應的依賴描述檔案是否存在,如果不存在則會輸出錯誤資訊給與提醒,後面會include 進來該檔案,最後使用我們熟悉的LIBS+=進行進入庫檔案

迴圈最後面兩句非常重要,這兩句起到停止迴圈作用,-=每次會從QTC_PLUGIN_DEPENS中去重done_plugins變數對應的外掛,最後直到QTC_PLUGIN_DEPENDS為空退出最外邊的迴圈

上面就是外掛依賴處理流程,動態庫依賴處理流程原理也類似,比如下面所示

done_libs =
for(ever) {
    isEmpty(QTC_LIB_DEPENDS): \
        break()
    done_libs += $$QTC_LIB_DEPENDS
    for(dep, QTC_LIB_DEPENDS) {
        include($$PWD/src/libs/$$dep/$${dep}_dependencies.pri)
        LIBS += -l$$qtLibraryName($$QTC_LIB_NAME)
    }
    QTC_LIB_DEPENDS = $$unique(QTC_LIB_DEPENDS)
    QTC_LIB_DEPENDS -= $$unique(done_libs)
}

可以看到整個過程幾行程式碼就可以解決整個專案工程之間的依賴問題,後面開發其它外掛和模組只需要按照這個規則編寫對應xx__dependencies.pri 檔案即可,後續的依賴載入會自動處理,可以減少很多工作量以及出錯問題

小試牛刀

我們來驗證下,編寫一個工具集模組Misc,該模組編譯後生成一個動態庫,為了儘可能的簡單,工程結構如下所示

檔案Misc_dependencies.pri列舉了該庫的依賴資訊

QTC_LIB_NAME = Misc

demo 全部工程原始碼下載可以訪問這裡[https://github.com/kevinlq/Qt-Creator-Opensource-Study]

多工程管理

上面提到了多工程依賴庫的一些管理方法,下面來看看Qt Creator工程中一些其它管理技巧

善於定義變數

工程中難免會有很多重複的配置,比如:

  • 某些檔案編譯時生成的臨時檔案路徑、編譯後動態庫、靜態庫、外掛的路徑;
  • 動態庫、靜態庫命名怎麼區分;
  • DebugRelease 版本下每個庫生成後的名字怎麼區分
  • 程式版本號怎麼在工程配置,然後程式碼中直接使用

其實稍微大一點的專案,會面臨很多基礎的問題,解決這些問題要善於定義變數

這句話怎麼理解呢?我們來看一些基本的例子就明白了

Debug 和 Release 區分

有時候我們不同編譯模式下生成的庫是不一樣的,呼叫第三方庫的時候也要注意這一點,那麼就要求程式在不同模式下編譯後生成的庫路徑放到不同路徑中

CONFIG(debug, debug|release):{
    DIR_COMPILEMODE = Debug
}
else:CONFIG(release, debug|release):{
    DIR_COMPILEMODE = Release
}

DESTDIR = $$PWD/$$DIR_COMPILEMODE

上面配置會在當前目錄下對應編譯模式下生成對應庫

檔案生成路徑定義

專案中一般都會定義檔案的輸出路徑,比如我有一個動態庫要輸出指定目錄,那麼對應的 pro 檔案該怎麼寫呢?

IDE_APP_NAME = QTC

isEmpty(IDE_BASE_BIN_PATH): IDE_BASE_BIN_PATH = $$QTC_PREFIX/$$IDE_APP_NAME

IDE_LIBRARY_PATH = $$IDE_BASE_BIN_PATH/bin
IDE_PLUGIN_PATH  = $$IDE_BASE_BIN_PATH/$$IDE_LIBRARY_BASENAME/$$IDE_APP_TARGET/plugins
IDE_DATA_PATH    = $$IDE_BASE_BIN_PATH/share/$$IDE_APP_TARGET
IDE_DOC_PATH     = $$IDE_BASE_BIN_PATH/share/doc/$$IDE_APP_TARGET
IDE_BIN_PATH     = $$IDE_BASE_BIN_PATH/bin

上述配置命令,在我們程式編譯後最終生成的路徑格式如下所示:

bin
    │  └─Win32
    │      ├─Debug
    │      │  └─QTCLearn03
    │      │      └─bin
    │      │              libMiscd.a
    │      │              libPluginsd.a
    │      │              Miscd.dll
    │      │              Pluginsd.dll
    │      │              QTCLearn03.exe
    │      │
    │      └─Release
    │          ├─QTC
    │          │  └─bin
    │          └─QTCLearn03
    │              └─bin

自動根據當前是 Debug 還是 Release模式生成到對應目錄,對程式不會造成干擾,而且每個模組外掛可以單獨設定其路徑,這樣做的好處就是分離清晰明確,便於管理和維護

針對外掛和動態庫可以分別處於不同的目錄,以依葫蘆畫瓢即可完成

PS: 如果想要完整的 pro 配置模板可以私信我,真的很好用,拿來就可以直接用。
事實上,上述程式碼你也可以學習完Qt Creator後自己也可以整理出來。

Qt Creator原始碼工程結構

原始碼雖然看起來很多很複雜,不過大概可以分為三個部分,libspluginsApp。如上圖我重點標紅的內容,我在上一篇學習筆記當中介紹過這三個部分分別是幹什麼的,詳細可以看上篇文章

Qt Creator 原始碼學習筆記02,認識框架結構結構

libs 工程封裝了一些外部使用的方法和函式,以動態庫的方式呈現,呼叫時引入動態庫加入標頭檔案即可。具體是怎麼加入的呢?閱讀原始碼你發現其它子工程並沒有直接引入,關鍵點還是上面提到的依賴管理方法

每個子工程都有自己的依賴配置檔案,比如aggregation_dependencies.pri,這個檔案必須要有,否則編譯時會報錯,提醒你缺少對應的依賴檔案

比如核心外掛管理庫依賴配置 extensionsystem_dependencies.pri

QTC_LIB_NAME = ExtensionSystem
QTC_LIB_DEPENDS += \
    aggregation \
    utils

你能很清晰的看出來這個庫依賴兩個庫,那麼在編譯它時這兩個庫必須先編譯

plugins是所有外掛功能的實現部分,包含核心外掛以及剩餘的擴充套件外掛

外掛也是一個個的動態庫,只不過每個外掛都是繼承自同一個介面或者說叫純虛類,各自實現一些必要的初始化函式,這樣才能統一管理和訪問控制

工程管理原理還是一樣的手法,每個外掛都擁有一個配置檔案,比如核心外掛 coreplugin_dependencies.pri

QTC_PLUGIN_NAME = Core
QTC_LIB_DEPENDS += \
    aggregation \
    extensionsystem \
    utils

可以看出來該外掛依賴三個動態庫,那麼如果外掛要依賴呢,怎麼寫?也非常簡單,只需要新增依賴的外掛名字即可,比如「書籤」外掛依賴配置檔案

QTC_PLUGIN_NAME = Bookmarks
QTC_LIB_DEPENDS += \
    extensionsystem \
    utils
QTC_PLUGIN_DEPENDS += \
    projectexplorer \
    coreplugin \
    texteditor

可以看出多了一項QTC_PLUGIN_DEPENDS,需要依賴那些外掛只需要往後追加即可

這裡說到書籤外掛,是一個非常好用的功能,平時編寫除錯程式碼梳理流程非常有用,比如閱讀到某個關鍵函式,發現這個函式又呼叫了其它檔案的方法,跳過去後發現又呼叫其它功能函式,每次跳轉此時太多,想要返回初始位置檢視就不方便,有了書籤隨時點選書籤就可以跳轉回去了

PS:建議大家使用最新版本的Qt Creator,書籤會一直快取,直到你手動刪除了,有時候標記了書籤,但是下班後電腦關機了第二天開啟後發現書籤還在,直接開始幹活,效率非常高

app工程是程式主入口,會顯示載入三個動態庫,各個外掛的呼叫是在main.cpp函式裡面動態載入呼叫的

當然了,實際配置時你還要考慮各各個平臺下的一些相容性,比如 maclinux等平臺,如果你的軟體不涉及這些平臺那麼就不用考慮了

總結

通過閱讀開源的框架和專案,可以增加我們的見識,平時工作或者學習當中可能不注意或者想不到的一些技巧方法和程式設計思想在閱讀原始碼的過程中都可以看到,時間久了各方面都會有很大的提升

下一篇我們來學習看看Qt Creator核心外掛管理機制是如何實現的,也是學習的重點


相關閱讀

相關文章