Qt Creator 源码学习笔记 05,菜单栏是怎么实现插件化的?

阅读本文大概需要 6 分钟

对于一个多插件的 IDE 软件来说,支持界面扩展是必不可少的,今天我们来看看在 Qt Creator 当中是如何实现界面扩展的

概述

界面扩展无非就是在其它插件中访问修改主界面当中的一些菜单、参数,或者添加、删除某些菜单,目前很多大型软件都是支持插件化开发的

前几篇我们一起看了Qt Creator的主界面其实很简单,主界面包括一个菜单栏,模式工具栏,内容区域以及状态栏,如下图所示:

202112192158402.png

我们看到的其它丰富功能均是通过插件化实现的,今天我们详细学习下看看 QTC 当中菜单栏是怎么实现扩展的

实现原理

在学习代码之前我们可以想一想,如果让我们自己来实现应该如何实现,比如扩展一个Menu菜单?

既然其他插件要扩展,那么肯定需要访问核心插件创建的 menu 对象,那么就必须要有访问权限,那么核心插件定义的 menu 对象应该有哪些权限呢?

202202082214583.png

仔细回忆下我们刚开始学习 C/C++ 的时候老师就给我们说过,定义一个变量/对象要注意哪些关键点?

  • 变量/对象的名
  • 变量/对象的值
  • 变量/对象的作用域
  • 变量/对象的生命周期

所以我们要实现一个菜单也是需要考虑这几个方面,最关键的是这个对象的生命周期,外部要能访问该对象可以有好几种方式:暴露指针给外使用、提供注册接口、定义单例……,其实把 menu定义成一个单例是最便捷最灵活的一种方式了,类似下面这种

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

PS: 定义接口或者暴露指针也可以,只不过每次访问还要先访问核心插件对象,处理起来比较繁琐罢了

源码实现

好了,下面我们看下源码是怎么实现的

菜单管理代码主要在这个位置 : /Src/plugins/.coreplugin/actionmanager

202202082217903.png

文件虽然看着很多,不用担心,我们主要关心的类有这么几个:

  • ActionContainer
  • ActionContainerPrivate
  • MenuActionContainer
  • MenuBarActionContainer
  • ActionManager

这几个类之间继承关系如下所示:

202202082220274.png

黄色表示的类对内使用,外部看不到具体的实现,每个菜单都可以是一个 MenuActionContainer 对象,MenuBarActionContainer全局只有一份,相当于是一个容器来容纳所有的菜单

那么我们如何创建一个菜单呢?其中有专门管理创建、注册的类来实现,这是一个单例类

class CORE_EXPORT ActionManager : public QObject
{
    Q_OBJECT
public:
    static ActionManager *instance();
    
    // 注册菜单
    static ActionContainer *createMenu(Id id);
    
    // 注册菜单栏
    static ActionContainer *createMenuBar(Id id);
    
    // 注册管理某个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);
    
    ......
}

在这个单例类当中,主要有两个重要的数据结构用来存储创建的菜单对象,详细实现都在它的 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 来存储每个对象,当创建的菜单对象比较多时查找效率非常高,同时注意键值key 是一个自定义的字符串ID,由特殊规则构成的全局唯一的值

// 创建菜单
ActionContainer *ActionManager::createMenu(Id id)
{
    // 创建前先进行查找,已经存在了直接返回该对象
    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);
    
    // 绑定销毁信号,当菜单对象删除后从当前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));
}

其中有一个比较重要的数据结构 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;
};

这个类其实就是一个字符串 ID 的数组封装,各个菜单的标识、状态控制都用到了它,这个结构贯穿整个 Qt Creator插件系统,使用起来还是非常方便的

有了上面的结构,那么如何创建菜单以及子菜单呢,下面我们详细看下

创建 MenuBar

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

这里没啥好说的,和我们平时在QMainWindow当中创建方法一样,只不过这里创建细节统一封装管理起来了

创建菜单

下面我们以「文件」菜单为例看下创建过程

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

这两行代码就完成了「文件」菜单的创建,代码很简洁也非常容易理解,这里我们需要注意下几个常量定义技巧

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

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

所有的菜单都是通过字符串常量来区分的,这个常量相当于现实世界中我们每个人的身份证都是唯一的,而且都是有规律的

PS:看到这里再问大家一个问题,定义常量时,宏定义写法和上面的写法哪个好?为什么?欢迎讨论

#define G_FILE "QtCreator.Group.File"

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

到了这里,仅仅是创建了菜单,点击菜单后内容还是空的,我们接着继续看


void MainWindow::registerDefaultActions()
{
    // 从单例类中获取上一步创建的菜单容器类 
    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);
    
    // 创建每个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);
    
    ......
}

每个action创建后通过 addAction 添加到对应的菜单上即可,如果某个 action 还有子菜单,那么就需要先创建一个菜单,然后直接添加菜单即可,比如「最近访问的文件」

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);

任意一个action可以拥有多个子菜单,只需要在创建的时候根据递归关系选择创建action还是ActionContainer

测试

为了验证上述流程分析是否正确,我们可以编译一个测试插件,然后在该插件里面新创建一个菜单,分为下面几个流程:

  • 创建测试插件PluginDemo子工程;
  • 在插件初始化函数当中创建菜单;
  • 编译该插件,然后把该插件(动态库)拷贝到 QTC 对应插件目录下
  • 运行软件

创建插件编译后生成的目录结构如下所示:


202202132133546.png

可以看到我们测试插件路径和程序 exe是独立的

运行软件显示效果如下所示


202202102221250.png

可以看到整个代码不超过 10行就把创建的菜单添加到了主界面当中,使用起来目前看来还是很方便的,而且方便扩展,由于使用插件化和其它模块进行了解耦

相信大家也都看到了,QTC 插件系统当中比较重要的ID编号问题,这些编号都有固定的格式,而且每个ID无论从命名还是具体内容表达的意思都是显而易见的

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开头表示分组信息,比如文件菜单当中包含了:新建文件、打开文件、打开工程、保存文件……

总结

Qt Creator界面插件化内容还很多,本次只是简简单单地学习了菜单管理逻辑以及如何使用,如果想了解更多细节阅读对应源码即可

一款优秀的开源软件有很多内容值得我们反复去学习、理解、使用的,未来很长,我们继续……


PS:文中涉及到相关流程图以及对应源码,如果感兴趣可以后台私信发给你

如果觉得对你有帮助,欢迎留言互相交流学习

推荐阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容