閱讀本文大概需要 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)欄堰燎,如下圖所示:
我們看到的其它豐富功能均是通過(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)限呢?
仔細(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
文件雖然看著很多,不用擔(dān)心柑贞,我們主要關(guān)心的類有這么幾個(gè):
- ActionContainer
- ActionContainerPrivate
- MenuActionContainer
- MenuBarActionContainer
- ActionManager
這幾個(gè)類之間繼承關(guān)系如下所示:
黃色表示的類對(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ò)程
// 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)的文件」
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)如下所示:
可以看到我們測(cè)試插件路徑和程序 exe
是獨(dú)立的
運(yùn)行軟件顯示效果如下所示
可以看到整個(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í)
推薦閱讀