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

328 阅读7分钟

阅读本文大概需要 6 分钟

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

概述

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

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

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

实现原理

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

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

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

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

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

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

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

源码实现

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

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

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

  • ActionContainer
  • ActionContainerPrivate
  • MenuActionContainer
  • MenuBarActionContainer
  • ActionManager

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

黄色表示的类对内使用,外部看不到具体的实现,每个菜单都可以是一个 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当中创建方法一样,只不过这里创建细节统一封装管理起来了

创建菜单

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

    // 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 还有子菜单,那么就需要先创建一个菜单,然后直接添加菜单即可,比如「最近访问的文件」

    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 对应插件目录下
  • 运行软件

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

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

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

可以看到整个代码不超过 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:文中涉及到相关流程图以及对应源码,如果感兴趣可以后台私信发给你

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

推荐阅读