学透 Electron 自定义菜单

7,371 阅读6分钟

导语:近几年,随着 Electron/ NW.js 等技术的兴起,也催生了一批优秀的桌面端开发者工具,比如 VSCode、微信开发者工具、飞冰(ICE) 等等。对于开发者而言,桌面端开发者工具的优势是:可视化能力、操作系统层面的 API 访问、和良好的开发调试体验。因此,最近准备系统性的深入学习下 Electron 技术并且将学习的知识进行适当沉淀。本篇文章主要总结 Electron 的自定义菜单。

传统的 Web APP 的开发基本上不会涉及到菜单,但是在 Electron 里面它提供了对于菜单全面的控制,你可以通过 Menu、MenuItem 模块来创建应用所需的自定义菜单。这篇文章我们一起探讨下 Electron 中有哪些菜单种类,又是如何通过代码去自定义菜单的?

首先,我们一起看看基本的菜单介绍,方便大家对于基本的概念有初步的认识。

菜单介绍

Electron 里的菜单大体上分为三类:应用菜单、上下文菜单和 Dock 菜单(仅针对 OSX 系统)。

这里以微信开发者工具为例(微信开发者工具基于 NW.js 进行开发,主要出于 Windows XP兼容性考虑),来分别介绍这几种菜单的含义。打开微信开发者工具,可以通过下图,很清晰的发现3个菜单所处的位置。

这三种菜单的含义分别是:

  • 应用菜单:应用菜单通常位于应用程序的顶部,提供了用户可能用到的各种操作,如程序的快捷方式、常用的文件夹及系统命令等。
  • 上下文菜单:在应用里面点击右键看到的菜单。
  • Dock 菜单:只在 OSX 系统才有,通常功能较少,提供特别常用的功能。

了解了菜单的基本概念后,接下来我们一起看看如何通过代码去实现自定义菜单的功能。

应用菜单

首先看看应用菜单,Electron 默认会有一个标准的应用菜单,我们一起看看默认的应用菜单效果:

仔细分析下默认应用菜单包括的菜单结构如下:

如果你希望定制应用菜单,你需要自行实现整个菜单的定义。这里需要注意,应用菜单只能在 Electron 的主进程中进行访问。例如:

// main.js
const {
    app,
    Menu
 } = require('electron');

app.on('ready', () => {
    const appMenu = Menu.buildFromTemplate(menuTemplate);
    Menu.setApplicationMenu(appMenu);
});

这里面重点关注 app 的 ready 这段代码块,应用菜单通过 **Menu.setApplicationMenu **进行设置。接下来分别从菜单模板、分隔符、快捷键和子菜单几个方面来系统介绍下应用菜单的内容。

菜单模板:

菜单的 template 是一个对象数组,每个对象会定义一个独立的菜单,它会显示在应用菜单的 Bar 位置,显示的文字通过 label 属性进行定义。

以这段代码为例,我们定义了两个菜单,每个菜单都包含两个菜单项,菜单项就是我们点击菜单时下拉出来的内容。

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo'
            },
            {
                label: 'Redo'
            }
        ]
    },
    {
        label: 'View App',
        submenu: [
            {
                label: 'Reload'
            },
            {
                label: 'Toggle Full Screen'
            }
        ]
    }
];

对应的效果:

这里值得注意的是:对于 OSX 而言,应用菜单的第一个菜单项是应用程序的名字,会使得 Edit App 这个菜单被覆盖掉。因此,我们需要针对 OSX 进行特殊处理,处理的过程通常是:

if (process.platform === 'darwin') {
    template.unshift({
        label: app.getName(),
        submenu: [
            {
                label: 'Quit',
                accelerator: 'CmdOrCtrl+Q',
                click() {
                    app.quit();
                }
            }
        ]
    });
}

分隔符:

通过 type: 'separator' 可以在两个菜单项之间定义一个分隔符,分隔符的作用主要是将功能相似的菜单项分隔在一起,便于更好的操作。

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo'
            },
            {
                type: 'separator'
            },
            {
                label: 'Redo'
            }
        ]
    }
];

可以看到,Undo 和 Redo 之间出现了一个分隔符。

接下来,我们一起了解下常用的快捷键和内置的 role 功能。

快捷键:

快捷键我们日常开发过程中用得很多,比如 Ctrl + A 全选,Ctrl + C 复制,Ctrl + V 粘贴。可以供我们选择的快捷键有:

  • Command (简写Cmd)
  • Control(简写Ctrl)
  • CommandOrControl(简写CmdOrCtrl)
  • Alt
  • Option
  • AltGr
  • Shift
  • Super

我们把上面的代码修改一下,增加快捷键,快捷键通过 accelerator 属性进行定义。

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo',
                accelerator: 'CmdOrCtrl+Z'
            },
            {
                type: 'separator'
            },
            {
                label: 'Redo',
                accelerator: 'Shift+CmdOrCtrl+Z',
            }
        ]
    }
];

添加完快捷键后,可能你会问,点击某个菜单或者某个快捷键后如何触发相应的逻辑呢?这个可以通过编写 click() 自定义回调函数或者使用 Electron 内置的 role 进行指定。我们将上述代码继续修改:

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo',
                accelerator: 'CmdOrCtrl+Z',
                role: 'undo'
            },
            {
                type: 'separator'
            },
            {
                label: 'Redo',
                accelerator: 'Shift+CmdOrCtrl+Z',
                role: 'redo'
            }
        ]
    }
];

增加了 role 之后可以发现就有对应的操作效果了,Electron 的所有内置的 role 如下:

  • undo: 撤销
  • redo:重做
  • cut:剪切
  • copy:复制
  • paste:粘贴
  • pasteAndMatchStyle
  • selectAll:全选
  • delete:删除
  • minimize:当前窗口最小化
  • close:关闭当前窗口
  • quit:退出应用程序
  • reload:刷新当前窗口
  • forceReload:强制刷新当前窗口,忽略缓存
  • toggleDevTools:打开或者关闭 devtool
  • togglefullscreen:进行全屏切换
  • resetZoom:重置窗口大小
  • zoomIn:放大窗口的10%.
  • zoomOut:缩小窗口的10%.

完整的 Role 可以查看:electronjs.org/docs/api/me…

子菜单:

我们在前面的基础上增加一个新的菜单 Sub Menu,可以看到这个菜单里面的菜单项新增了 submenu 属性,通过这个属性可以继续定义子菜单,此处我们定义了 Submenu item1 和 Submenu item2。

const template = [
    {
        label: 'Edit App',
        submenu: [
            {
                label: 'Undo',
                accelerator: 'CmdOrCtrl+Z',
                role: 'undo'
            },
            {
                type: 'separator'
            },
            {
                label: 'Redo',
                accelerator: 'Shift+CmdOrCtrl+Z',
                role: 'redo'
            }
        ]
    },
    {
        label: 'Sub Menu',
        submenu: [
            {
                label: 'Submenu item',
                submenu: [
                    {
                        label: 'Submenu item1'
                    },
                    {
                        label: 'Submenu item2'
                    }
                ]
            }
        ]
    },
];

子菜单的效果如下:

到这里,应用菜单这个最重要的内容就介绍完了,接下来我们看看上下文菜单这个部分。

上下文菜单

上下文菜单(context menu)就是我们通常说的右键菜单,文章开头有展示效果。需要注意的是:上下文菜单,需要在渲染进程中进行实现。在渲染进程中是需要通过remote模块调用主进程中的模块。

实现上下文菜单很简单,只需要监听到 contextmenu 事件,然后将菜单展示出来即可。

//renderer.js
const { remote } = require('electron');
const { Menu } = remote;

const createContextMenu = () => {
    const contextTemplate = [
        {
            label: 'Cut',
            role: 'cut'
        },
        {
            label: 'Copy',
            role: 'copy'
        }
    ];
    const contextMenu = Menu.buildFromTemplate(contextTemplate);
    return contextMenu;
}

window.addEventListener('contextmenu', (event) => {
    event.preventDefault();
    const contextMenu = createContextMenu();
    contextMenu.popup({
        window: remote.getCurrentWindow()
    });
}, false);

Dock菜单

最后,我们一起看看 Dock 菜单,Dock 的菜单实现也是在主进程中,实现思路和前面基本类似,核心是通过 app.dock.setMenu 这个 API 进行实现的。

// main.js
const createDockMenu = () => {
    const dockTempalte = [
        {
            label: 'New Window',
            click () {
                console.log('New Window');
            }
        }, {
            label: 'New Window with Settings',
            submenu: [
                { label: 'Basic' },
                { label: 'Pro' }
            ]
        },
        {
            label: 'New Command...'
        }
    ];

    const dockMenu = Menu.buildFromTemplate(dockTempalte);
    app.dock.setMenu(dockMenu);
}

app.on('ready', function() {
    createDockMenu();
});

Dock 菜单的效果如下:

至此,这篇文章到这里就结束了,感谢您的阅读。后面的文章会涉及到对话框、 IPC 通信、Electron 应用的测试、打包、发布和自动更新等内容。


我的个人博客:github.com/cpselvis/bl…

想学习更多干货内容可以扫码关注我的公众号:推送频率每周一篇