Qt Creator 源碼學(xué)習(xí)筆記 05窖梁,菜單欄是怎么實(shí)現(xiàn)插件化的?

閱讀本文大概需要 6 分鐘

對(duì)于一個(gè)多插件的 IDE 軟件來(lái)說(shuō)表牢,支持界面擴(kuò)展是必不可少的窄绒,今天我們來(lái)看看在 Qt Creator 當(dāng)中是如何實(shí)現(xiàn)界面擴(kuò)展的

概述

界面擴(kuò)展無(wú)非就是在其它插件中訪問(wèn)修改主界面當(dāng)中的一些菜單贝次、參數(shù)崔兴,或者添加、刪除某些菜單,目前很多大型軟件都是支持插件化開發(fā)的

前幾篇我們一起看了Qt Creator的主界面其實(shí)很簡(jiǎn)單敲茄,主界面包括一個(gè)菜單欄位谋,模式工具欄,內(nèi)容區(qū)域以及狀態(tài)欄堰燎,如下圖所示:

202112192158402.png

我們看到的其它豐富功能均是通過(guò)插件化實(shí)現(xiàn)的掏父,今天我們?cè)敿?xì)學(xué)習(xí)下看看 QTC 當(dāng)中菜單欄是怎么實(shí)現(xiàn)擴(kuò)展的

實(shí)現(xiàn)原理

在學(xué)習(xí)代碼之前我們可以想一想,如果讓我們自己來(lái)實(shí)現(xiàn)應(yīng)該如何實(shí)現(xiàn)秆剪,比如擴(kuò)展一個(gè)Menu菜單赊淑?

既然其他插件要擴(kuò)展,那么肯定需要訪問(wèn)核心插件創(chuàng)建的 menu 對(duì)象仅讽,那么就必須要有訪問(wèn)權(quán)限陶缺,那么核心插件定義的 menu 對(duì)象應(yīng)該有哪些權(quán)限呢?

202202082214583.png

仔細(xì)回憶下我們剛開始學(xué)習(xí) C/C++ 的時(shí)候老師就給我們說(shuō)過(guò)洁灵,定義一個(gè)變量/對(duì)象要注意哪些關(guān)鍵點(diǎn)饱岸?

  • 變量/對(duì)象的名
  • 變量/對(duì)象的值
  • 變量/對(duì)象的作用域
  • 變量/對(duì)象的生命周期

所以我們要實(shí)現(xiàn)一個(gè)菜單也是需要考慮這幾個(gè)方面,最關(guān)鍵的是這個(gè)對(duì)象的生命周期徽千,外部要能訪問(wèn)該對(duì)象可以有好幾種方式:暴露指針給外使用苫费、提供注冊(cè)接口、定義單例……双抽,其實(shí)把 menu定義成一個(gè)單例是最便捷最靈活的一種方式了百框,類似下面這種

class MenuManager
{
    public:
    static MenuManager * instance();
    
    ......
}

PS: 定義接口或者暴露指針也可以,只不過(guò)每次訪問(wèn)還要先訪問(wèn)核心插件對(duì)象牍汹,處理起來(lái)比較繁瑣罷了

源碼實(shí)現(xiàn)

好了琅翻,下面我們看下源碼是怎么實(shí)現(xiàn)的

菜單管理代碼主要在這個(gè)位置 : /Src/plugins/.coreplugin/actionmanager

202202082217903.png

文件雖然看著很多,不用擔(dān)心柑贞,我們主要關(guān)心的類有這么幾個(gè):

  • ActionContainer
  • ActionContainerPrivate
  • MenuActionContainer
  • MenuBarActionContainer
  • ActionManager

這幾個(gè)類之間繼承關(guān)系如下所示:

202202082220274.png

黃色表示的類對(duì)內(nèi)使用方椎,外部看不到具體的實(shí)現(xiàn),每個(gè)菜單都可以是一個(gè) MenuActionContainer 對(duì)象钧嘶,MenuBarActionContainer全局只有一份棠众,相當(dāng)于是一個(gè)容器來(lái)容納所有的菜單

那么我們?nèi)绾蝿?chuàng)建一個(gè)菜單呢?其中有專門管理創(chuàng)建有决、注冊(cè)的類來(lái)實(shí)現(xiàn)闸拿,這是一個(gè)單例類

class CORE_EXPORT ActionManager : public QObject
{
    Q_OBJECT
public:
    static ActionManager *instance();
    
    // 注冊(cè)菜單
    static ActionContainer *createMenu(Id id);
    
    // 注冊(cè)菜單欄
    static ActionContainer *createMenuBar(Id id);
    
    // 注冊(cè)管理某個(gè)action
    static Command *registerAction(QAction *action, Id id,
                                   const Context &context = Context(Constants::C_GLOBAL),
                                   bool scriptable = false);
    static void unregisterAction(QAction *action, Id id);
    
    ......
}

在這個(gè)單例類當(dāng)中,主要有兩個(gè)重要的數(shù)據(jù)結(jié)構(gòu)用來(lái)存儲(chǔ)創(chuàng)建的菜單對(duì)象书幕,詳細(xì)實(shí)現(xiàn)都在它的 D指針里面

class ActionManagerPrivate : public QObject
{
    Q_OBJECT
public:
    typedef QHash<Id, Action *> IdCmdMap;
    typedef QHash<Id, ActionContainerPrivate *> IdContainerMap;
    ......
    
    IdCmdMap m_idCmdMap;
    IdContainerMap m_idContainerMap;
}

使用哈希Map 來(lái)存儲(chǔ)每個(gè)對(duì)象新荤,當(dāng)創(chuàng)建的菜單對(duì)象比較多時(shí)查找效率非常高,同時(shí)注意鍵值key 是一個(gè)自定義的字符串ID台汇,由特殊規(guī)則構(gòu)成的全局唯一的值

// 創(chuàng)建菜單
ActionContainer *ActionManager::createMenu(Id id)
{
    // 創(chuàng)建前先進(jìn)行查找苛骨,已經(jīng)存在了直接返回該對(duì)象
    const ActionManagerPrivate::IdContainerMap::const_iterator it = d->m_idContainerMap.constFind(id);
    if (it !=  d->m_idContainerMap.constEnd())
        return it.value();

    MenuActionContainer *mc = new MenuActionContainer(id);

    d->m_idContainerMap.insert(id, mc);
    
    // 綁定銷毀信號(hào)篱瞎,當(dāng)菜單對(duì)象刪除后從當(dāng)前map中移除
    connect(mc, &QObject::destroyed, d, &ActionManagerPrivate::containerDestroyed);

    return mc;
}

void ActionManagerPrivate::containerDestroyed()
{
    ActionContainerPrivate *container = static_cast<ActionContainerPrivate *>(sender());
    m_idContainerMap.remove(m_idContainerMap.key(container));
}

其中有一個(gè)比較重要的數(shù)據(jù)結(jié)構(gòu) Context

class CORE_EXPORT Context
{
public:
    Context() {}

    explicit Context(Id c1) { add(c1); }
    Context(Id c1, Id c2) { add(c1); add(c2); }
    Context(Id c1, Id c2, Id c3) { add(c1); add(c2); add(c3); }
    ......
    void add(const Context &c) { d += c.d; }
    void add(Id c) { d.append(c); }

private:
    QList<Id> d;
};

這個(gè)類其實(shí)就是一個(gè)字符串 ID 的數(shù)組封裝,各個(gè)菜單的標(biāo)識(shí)痒芝、狀態(tài)控制都用到了它俐筋,這個(gè)結(jié)構(gòu)貫穿整個(gè) Qt Creator插件系統(tǒng),使用起來(lái)還是非常方便的

有了上面的結(jié)構(gòu)严衬,那么如何創(chuàng)建菜單以及子菜單呢澄者,下面我們?cè)敿?xì)看下

創(chuàng)建 MenuBar

    ActionContainer *menubar = ActionManager::createMenuBar(Constants::MENU_BAR);
    // System menu bar on Mac
    if (!HostOsInfo::isMacHost()) 
    {
        setMenuBar(menubar->menuBar());
    }

這里沒啥好說(shuō)的,和我們平時(shí)在QMainWindow當(dāng)中創(chuàng)建方法一樣请琳,只不過(guò)這里創(chuàng)建細(xì)節(jié)統(tǒng)一封裝管理起來(lái)了

創(chuàng)建菜單

下面我們以「文件」菜單為例看下創(chuàng)建過(guò)程

202202082238269.png
    // File Menu
    ActionContainer *filemenu = ActionManager::createMenu(Constants::M_FILE);
    menubar->addMenu(filemenu, Constants::G_FILE);
    filemenu->menu()->setTitle(tr("&File"));

這兩行代碼就完成了「文件」菜單的創(chuàng)建粱挡,代碼很簡(jiǎn)潔也非常容易理解,這里我們需要注意下幾個(gè)常量定義技巧

const char M_FILE[]                = "QtCreator.Menu.File";

// Main menu bar groups
const char G_FILE[]                = "QtCreator.Group.File";

所有的菜單都是通過(guò)字符串常量來(lái)區(qū)分的俄精,這個(gè)常量相當(dāng)于現(xiàn)實(shí)世界中我們每個(gè)人的身份證都是唯一的抱怔,而且都是有規(guī)律的

PS:看到這里再問(wèn)大家一個(gè)問(wèn)題,定義常量時(shí)嘀倒,宏定義寫法和上面的寫法哪個(gè)好屈留?為什么?歡迎討論

#define G_FILE "QtCreator.Group.File"

const char G_FILE[]                = "QtCreator.Group.File";

到了這里测蘑,僅僅是創(chuàng)建了菜單灌危,點(diǎn)擊菜單后內(nèi)容還是空的,我們接著繼續(xù)看


void MainWindow::registerDefaultActions()
{
    // 從單例類中獲取上一步創(chuàng)建的菜單容器類 
    ActionContainer *mfile = ActionManager::actionContainer(Constants::M_FILE);
    
    // 添加分隔符
    mfile->addSeparator(Constants::G_FILE_SAVE);
    mfile->addSeparator(Constants::G_FILE_PRINT);
    mfile->addSeparator(Constants::G_FILE_CLOSE);
    mfile->addSeparator(Constants::G_FILE_OTHER);
    
    // 創(chuàng)建每個(gè)action
    QIcon icon = QIcon::fromTheme(QLatin1String("document-new"), Utils::Icons::NEWFILE.icon());
    m_newAction = new QAction(icon, tr("&New File or Project..."), this);
    cmd = ActionManager::registerAction(m_newAction, Constants::NEW);
    cmd->setDefaultKeySequence(QKeySequence::New);
    mfile->addAction(cmd, Constants::G_FILE_NEW);
    
    ......
}

每個(gè)action創(chuàng)建后通過(guò) addAction 添加到對(duì)應(yīng)的菜單上即可碳胳,如果某個(gè) action 還有子菜單勇蝙,那么就需要先創(chuàng)建一個(gè)菜單,然后直接添加菜單即可挨约,比如「最近訪問(wèn)的文件」

202202082311314.png
    ActionContainer *ac = ActionManager::createMenu(Constants::M_FILE_RECENTFILES);
    mfile->addMenu(ac, Constants::G_FILE_OPEN);
    ac->menu()->setTitle(tr("Recent &Files"));
    ac->setOnAllDisabledBehavior(ActionContainer::Show);

任意一個(gè)action可以擁有多個(gè)子菜單味混,只需要在創(chuàng)建的時(shí)候根據(jù)遞歸關(guān)系選擇創(chuàng)建action還是ActionContainer

測(cè)試

為了驗(yàn)證上述流程分析是否正確,我們可以編譯一個(gè)測(cè)試插件诫惭,然后在該插件里面新創(chuàng)建一個(gè)菜單翁锡,分為下面幾個(gè)流程:

  • 創(chuàng)建測(cè)試插件PluginDemo子工程;
  • 在插件初始化函數(shù)當(dāng)中創(chuàng)建菜單夕土;
  • 編譯該插件馆衔,然后把該插件(動(dòng)態(tài)庫(kù))拷貝到 QTC 對(duì)應(yīng)插件目錄下
  • 運(yùn)行軟件

創(chuàng)建插件編譯后生成的目錄結(jié)構(gòu)如下所示:


202202132133546.png

可以看到我們測(cè)試插件路徑和程序 exe是獨(dú)立的

運(yùn)行軟件顯示效果如下所示


202202102221250.png

可以看到整個(gè)代碼不超過(guò) 10行就把創(chuàng)建的菜單添加到了主界面當(dāng)中,使用起來(lái)目前看來(lái)還是很方便的怨绣,而且方便擴(kuò)展角溃,由于使用插件化和其它模塊進(jìn)行了解耦

相信大家也都看到了,QTC 插件系統(tǒng)當(dāng)中比較重要的ID編號(hào)問(wèn)題篮撑,這些編號(hào)都有固定的格式减细,而且每個(gè)ID無(wú)論從命名還是具體內(nèi)容表達(dá)的意思都是顯而易見的

const char M_FILE[]                = "QtCreator.Menu.File";
const char M_EDIT[]                = "QtCreator.Menu.Edit";
const char M_EDIT_ADVANCED[]       = "QtCreator.Menu.Edit.Advanced";
const char M_TOOLS[]               = "QtCreator.Menu.Tools";

const char G_FILE_NEW[]            = "QtCreator.Group.File.New";
const char G_FILE_OPEN[]           = "QtCreator.Group.File.Open";
const char G_FILE_PROJECT[]        = "QtCreator.Group.File.Project";
const char G_FILE_SAVE[]           = "QtCreator.Group.File.Save";
  • M開頭表示菜單名字,比如文件赢笨、編輯未蝌、視圖驮吱、構(gòu)建……
  • G開頭表示分組信息,比如文件菜單當(dāng)中包含了:新建文件树埠、打開文件糠馆、打開工程嘶伟、保存文件……

總結(jié)

Qt Creator界面插件化內(nèi)容還很多怎憋,本次只是簡(jiǎn)簡(jiǎn)單單地學(xué)習(xí)了菜單管理邏輯以及如何使用,如果想了解更多細(xì)節(jié)閱讀對(duì)應(yīng)源碼即可

一款優(yōu)秀的開源軟件有很多內(nèi)容值得我們反復(fù)去學(xué)習(xí)九昧、理解绊袋、使用的,未來(lái)很長(zhǎng)铸鹰,我們繼續(xù)……


PS:文中涉及到相關(guān)流程圖以及對(duì)應(yīng)源碼癌别,如果感興趣可以后臺(tái)私信發(fā)給你

如果覺得對(duì)你有幫助,歡迎留言互相交流學(xué)習(xí)

推薦閱讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末逊谋,一起剝皮案震驚了整個(gè)濱河市擂达,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌胶滋,老刑警劉巖板鬓,帶你破解...
    沈念sama閱讀 221,820評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異究恤,居然都是意外死亡俭令,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門部宿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)唤蔗,“玉大人,你說(shuō)我怎么就攤上這事窟赏〖斯瘢” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵涯穷,是天一觀的道長(zhǎng)棍掐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)拷况,這世上最難降的妖魔是什么作煌? 我笑而不...
    開封第一講書人閱讀 59,714評(píng)論 1 297
  • 正文 為了忘掉前任掘殴,我火速辦了婚禮,結(jié)果婚禮上粟誓,老公的妹妹穿的比我還像新娘奏寨。我一直安慰自己,他們只是感情好鹰服,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評(píng)論 6 397
  • 文/花漫 我一把揭開白布病瞳。 她就那樣靜靜地躺著,像睡著了一般悲酷。 火紅的嫁衣襯著肌膚如雪套菜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,328評(píng)論 1 310
  • 那天设易,我揣著相機(jī)與錄音逗柴,去河邊找鬼。 笑死顿肺,一個(gè)胖子當(dāng)著我的面吹牛戏溺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播屠尊,決...
    沈念sama閱讀 40,897評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼旷祸,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了知染?” 一聲冷哼從身側(cè)響起肋僧,我...
    開封第一講書人閱讀 39,804評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎控淡,沒想到半個(gè)月后嫌吠,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,345評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡掺炭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評(píng)論 3 340
  • 正文 我和宋清朗相戀三年辫诅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涧狮。...
    茶點(diǎn)故事閱讀 40,561評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炕矮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出者冤,到底是詐尸還是另有隱情肤视,我是刑警寧澤,帶...
    沈念sama閱讀 36,238評(píng)論 5 350
  • 正文 年R本政府宣布涉枫,位于F島的核電站邢滑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏愿汰。R本人自食惡果不足惜困后,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評(píng)論 3 334
  • 文/蒙蒙 一乐纸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧摇予,春花似錦汽绢、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至救鲤,卻和暖如春久窟,著一層夾襖步出監(jiān)牢的瞬間秩冈,已是汗流浹背本缠。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留入问,地道東北人丹锹。 一個(gè)月前我還...
    沈念sama閱讀 48,983評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像芬失,于是被迫代替她去往敵國(guó)和親楣黍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容