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

閱讀本文大概需要 6 分鐘

在上一篇大概了解了關於Qt Creator 基礎知識後[1],本篇先學習下框架基本結構,這樣能夠清晰的知道這個框架當中包含哪些文件、文件夾、工程文件,這些文件分別代表什麼意思以及有什麼作用

文件結構

打開下載好的源碼,如下目錄所示

可以看出來,文件和文件夾很多,不要被這些表面嚇著,我們真正需要關心的沒有幾個,需要重點關注的我加粗顯示了

  • bin文件夾
  • dist 文件夾
  • doc 文件夾
  • qbs 文件夾
  • scripts 文件夾
  • share 文件夾
  • src 文件夾
  • tests 文件夾
  • docs.pri
  • qtcreator.pri
  • qtcreator.pro
  • qtcreator.qbs
  • qtcreatordata.pri
  • README.md

這裡我們主要要關注src文件夾,這個下面是這個框架的源碼,其它的文件夾先不看

qtcreator.pri文件是項目工程中的一些通用配置,比如版本號,一些庫的輸出路徑定義,每個插件或者子工程都會包含該配置文件,方便直接配置工程一些變數(具體怎麼配置,後面會講解到)

qtcreator.pro文件是主工程文件,要打開編譯源碼也是需要打開該工程文件進行載入的

PS: 涉及到 qbs 相關內容可以不用關注了,Qt Build Suite 也是一種跨平台的編譯工具,目前使用較少無需關注

框架結構

下面來詳細看下工程結構是如何管理的,以及整個框架原理

使用 Qt Creator 打開工程後你會發現有很多子工程項目,這個時候不要亂、不要怕,我們目前只需要關心三個部分就可以了

  • libs
  • plugins
  • app

libs部分

libs工程下面包含了常用的一些通用方法,我們目前關注三個即可

Aggregation工程

這個類提供了「打包」功能,可以將很多組件打包成一個整體,整個理解起來有點抽象,你可以理解為將多個對象封裝成一個對象,這個對象對外提供了所有對象的介面屬性和方法

  • Aggregation 集合內部每個組件對象都可以互相轉化
  • Aggregation 集合內每個對象的生命周期被綁定在了一起,即一個在全部在,一個被刪除析構那麼其餘的組件也就會被析構

extensionsystem工程

這個類實現了插件的管理功能,是整個框架的核心部分,所有的插件生命周期管理都在這個類裡面實現

  • IPlugin 插件基類,後面所有的插件都是繼承自它來實現所有功能的,有三個重點方法需要關注
    virtual bool initialize(const QStringList &arguments, QString *errorString) = 0;
    virtual void extensionsInitialized() = 0;
    virtual bool delayedInitialize() { return false; }

插件的初始化,外部依賴初始化,延遲初始化,這三個虛函數用來初始化每個插件各自的一些資源資訊。外部依賴那個也尤為重要,比如我們某個插件同時依賴多個其它插件,那麼就需要在這裡處理等待其它插件載入完成才算完成

  • PluginManager 插件管理單例類,整個框架只有一份,負責框架插件的管理,隨著程式退出它的聲明周期才結束
  • PluginManagerPrivate 插件管理具體實現邏輯類,看名字就很清楚,典型的P-D指針關係,這樣是為了把插件系統擴展的具體實現隱藏不給外部暴露,這種技巧在後面很多程式碼中經常會見到,也是值的我們去學習
  • PluginSpec 插件核心類,該類實現插件的所有屬性
class EXTENSIONSYSTEM_EXPORT PluginSpec
{
public:
    enum State { Invalid, Read, Resolved, Loaded, Initialized, Running, Stopped, Deleted};

    ~PluginSpec();
    
    // 插件名字
    QString name() const;
    // 插件版本
    QString version() const;
    // 插件兼容版本
    QString compatVersion() const;

    // 插件提供者
    QString vendor() const;

    // 插件版權
    QString copyright() const;

    // 插件協議
    QString license() const;

    // 插件描述
    QString description() const;

    // 插件主頁 URL
    QString url() const;
    
    // 插件類別,用於在介面分組顯示插件資訊
    QString category() const;
}

每個插件(每個動態庫)都有一份該對象,用來記錄該插件的所有屬性資訊,這些屬性資訊是通過 json配置文件讀入的,這些資訊被稱為插件的「元資訊」,後面關注插件實現會提到

utils 工程

這個工程裡面封裝了一些基礎功能演算法類,比如文件操作、數據排序操作、json交互操作、字元串操作集合等,還有一些基礎封裝控制項實現也在這個裡面

比如後面要提到的核心插件主窗口QMainWindow類,基類就在在這裡

class QTCREATOR_UTILS_EXPORT AppMainWindow : public QMainWindow
{
    Q_OBJECT
public:
    AppMainWindow();

public slots:
    void raiseWindow();

signals:
    void deviceChange();

#ifdef Q_OS_WIN
protected:
    virtual bool winEvent(MSG *message, long *result);
    virtual bool event(QEvent *event);
#endif

private:
    const int m_deviceEventId;
};

這裡主要是一些事件變化後通知外部處理,比如這裡如果主題發生改變發送對應訊號出去,設備發生改變(插拔光碟機等)發出一個設備改變事件到 Qt 事件隊列去處理

plugin 部分

這部分是每個插件實現部分,重點需要關注核心插件corePlugin的實現,其它插件都是要依賴核心插件來實現業務功能

在這個插件裡面主要初始化了主窗口、菜單管理類實例以及一些模式管理對象初始化

後面我們會看到各種各樣的插件,比如你打開Qt Creator的時候首頁顯示的內容,也是單獨的一個插件,名字叫做weilcome

每個插件都有一個標識ID,用來區分是你自己寫的插件,防止別人惡意修改插件

Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Core.json")
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Welcome.json")

每個插件還有一個對應的元數據描述配置文件,這個文件配置了該插件的一些基本資訊,比如插件名字、版本、所有權、依賴那些插件等,這些配置資訊在編譯時會寫進該插件動態庫當中,採用的是Qt的元對象技術來實現的,這樣在插件載入運行時就能通過反射動態獲取這些資訊,繼而用來進行一些插件之間載入關係的驗證

一個簡單的配置描述如下所示

{
    \"Name\" : \"Welcome\",
    \"Version\" : \"$$QTCREATOR_VERSION\",
    \"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\",
    \"Vendor\" : \"The Qt Company Ltd\",
    \"Copyright\" : \"(C) $$QTCREATOR_COPYRIGHT_YEAR The Qt Company Ltd\",
    \"License\" : [ \"Commercial Usage\",
                  \"\",
                  \"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and The Qt Company.\",
                  \"\",
                  \"GNU General Public License Usage\",
                  \"\",
                  \"Alternatively, this plugin may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this plugin. Please review the following information to ensure the GNU General Public License requirements will be met: //www.gnu.org/licenses/gpl-3.0.html.\"
    ],
    \"Category\" : \"Qt Creator\",
    \"Description\" : \"Secondary Welcome Screen Plugin.\",
    \"Url\" : \"//www.qt.io\",
    $$dependencyList
}

其中很關鍵的是一些變數值,比如$$QTCREATOR_VERSION,通過這個變數直接可以讀取到我們在工程qtcreator.pri中定義的變數值,繼而快速統一載入顯示,其它變數值獲取類似

其次,需要關注的是每個插件的配置依賴文件比如welcome_dependencies.pri,該文件中包含了依賴那些庫那些插件

# 插件名字
QTC_PLUGIN_NAME = Welcome

# 插件依賴的庫
QTC_LIB_DEPENDS += \
    extensionsystem \
    utils
    
# 插件依賴的插件
QTC_PLUGIN_DEPENDS += \
    coreplugin

某個插件依賴那些插件和動態庫,只需要在對應位置追加其名字即可,工程配置文件會自動進行載入,這樣編寫可以減少很多重複工作,而且插件依賴關係也很清楚的看到

app 部分

這個部分是程式入口實現部分,這裡主要是獲取插件路徑,初始化插件、配置文件,載入每個插件,如果都沒有錯誤,那麼初始化完成後主介面就會顯示出來,直接看主函數入口看就行

關鍵部分是插件管理器的初始化,設置插件搜索路徑後對每個插件進行初始化操作

    PluginManager pluginManager;
    PluginManager::setPluginIID(QLatin1String("org.qt-project.Qt.QtCreatorPlugin"));
    PluginManager::setGlobalSettings(globalSettings);
    PluginManager::setSettings(settings);
    ......
    const QStringList pluginPaths = getPluginPaths() + customPluginPaths;
    PluginManager::setPluginPaths(pluginPaths);
    
    ......
    PluginManager::loadPlugins();

在這裡還有一個需要注意的地方,就是這個文件app_version.h.in

這個是一個模板文件,qmake載入執行完畢後,會在臨時目錄下生成對應的頭文件app_version.h

#pragma once

namespace Core {
namespace Constants {

#define STRINGIFY_INTERNAL(x) #x
#define STRINGIFY(x) STRINGIFY_INTERNAL(x)

const char IDE_DISPLAY_NAME[] = \"$${IDE_DISPLAY_NAME}\";
const char IDE_ID[] = \"$${IDE_ID}\";
const char IDE_CASED_ID[] = \"$${IDE_CASED_ID}\";

#define IDE_VERSION $${QTCREATOR_VERSION}
#define IDE_VERSION_STR STRINGIFY(IDE_VERSION)
#define IDE_VERSION_DISPLAY_DEF $${QTCREATOR_DISPLAY_VERSION}

#define IDE_VERSION_MAJOR $$replace(QTCREATOR_VERSION, "^(\\d+)\\.\\d+\\.\\d+(-.*)?$", \\1)
#define IDE_VERSION_MINOR $$replace(QTCREATOR_VERSION, "^\\d+\\.(\\d+)\\.\\d+(-.*)?$", \\1)
#define IDE_VERSION_RELEASE $$replace(QTCREATOR_VERSION, "^\\d+\\.\\d+\\.(\\d+)(-.*)?$", \\1)

const char * const IDE_VERSION_LONG      = IDE_VERSION_STR;
const char * const IDE_VERSION_DISPLAY   = STRINGIFY(IDE_VERSION_DISPLAY_DEF);
const char * const IDE_AUTHOR            = \"The Qt Company Ltd\";
const char * const IDE_YEAR              = \"$${QTCREATOR_COPYRIGHT_YEAR}\";

#ifdef IDE_REVISION
const char * const IDE_REVISION_STR      = STRINGIFY(IDE_REVISION);
#else
const char * const IDE_REVISION_STR      = \"\";
#endif
...

} // Constants
} // Core

這個模板文件定義了一些常量,某些變數值引用的是宏定義,最後編譯後宏定義會被替換掉真正的值,在我們程式碼中引入時真正起作用,更加詳細使用過程後面統一分析pro文件技巧時會提到

總結

學習到這裡,已經大概清楚了Qt Creator框架的基本結構了,首先是一些基本庫,這些動態庫封裝了一些基本功能和用法,方便在多個模組重複調用使用,其次是插件管理系統的實現,主要包含插件對象聲明周期管理,插件載入、插件卸載、插件直接依賴關係處理

比如有插件A、B、C,C插件現在同時依賴於插件A和B,那麼在載入時就需要特殊考慮

最後就是多個插件的初始化,主窗口和菜單組件管理類,方便拓展到其它插件進行訪問管理

整個QTC插件系統是由一個個動態庫構成的,每個插件互相配合實現了這樣一個複雜的跨平台的IDE,仔細研究下就可以發現很多奇妙的用法和知識

相關閱讀

  • Qt Creator 學習筆記01,初識 QTC[1:1]

  1. //kevinlq.com/2021/11/01/learn01_studyQTC/ ↩︎ ↩︎