顶部菜单的编辑
桌面应用的菜单是我们很常用的一个功能,大到各种应用,小到一个小小的记事本,都有自己的菜单,那么接下来就看看如何实现一个菜单
传入模板生成菜单
Electron提供了一个模块Menu来实现菜单功能,我们直接通过一个demo来演示
首先创建如图的目录
这里menu文件夹下的menu.js是用来写菜单的信息,包括菜单的内容,以及菜单的点击事件触发的回调,这里实际上是在主进程中调用的,但是为了可以区分功能和复用,我写在menu.js中,并在主进程中require它
主进程index.js
const { app, BrowserWindow } = require('electron')
// app:管理Electron应用程序的声明周期
// BrowserWindow负责创建窗口
let mainWindow = null // 要打开的主窗口应用
app.on('ready', () => { // 监听准备事件
mainWindow = new BrowserWindow({ // 传入对象设置窗口参数,这里设置了宽和高以及允许使用node的API
width: 500,
height: 500,
webPreferences: {
nodeIntegration: true
}
})
require('./menu/menu.js')
mainWindow.loadFile('index.html')
mainWindow.on('closed', () => { // 监听关闭时间
mainWindow = null // 将窗口置为null,否则的话内存会一直被占用
})
})
这里menu.js也不做过多解释了,其实看代码也已经很清晰了,唯一要说的是setApplicationMenu方法,它在macOS环境和Windows/Linux环境中的表现不同,在macOS中,会把传入的菜单menu设置为应用程序菜单,而在Windows/Linux中,menu会被设置为每个应用的顶部菜单。
menu.js
const { Menu, BrowserWindow } = require('electron')
let template = [{
label: '文件',
submenu: [{
label: '新建窗口',
click: () => { // 添加点击对应的操作,这里为打开新的窗口
let newWin = new BrowserWindow({
width: 500,
height: 500,
webPreferences: {
nodeIntegration: true
}
})
newWin.loadFile('index2.html')
newWin.on('closed', () => {
newWin = null
})
}
},
{ label: '关闭窗口' }
]
}, {
label: '编辑',
submenu: [
{ label: '撤销' },
{ label: '恢复' }
]
}]
const menu = Menu.buildFromTemplate(template) // 根据template生成一个Menu对象
Menu.setApplicationMenu(menu) // 根据menu生成菜单
点击文件中的新建窗口,就可以打开一个新的桌面应用
通过添加MenuItem来创建菜单
除了使用模板的形式外,我们还可以通过往Menu对象中添加MenuItem来添加新的菜单项,因为Menu.buildFromTemplate会返回一个Menu对象,所以这里我们在menu.js中添加
const menu = Menu.buildFromTemplate(template) // 根据template生成一个Menu对象
menu.append(new MenuItem({label:'click',click(){console.log('click')}})) // 在menu中添加新的菜单项
Menu.setApplicationMenu(menu) // 根据menu生成菜单
然后运行该应用,点击click,发现控制台打印出了click
这种做法的好处在于,我们可以动态地修改我们的菜单项,这样根据使用应用的人权限不同,我们可以提供不同的菜单项比如下面这个小demo,我在global.json中添加了identity属性,然后在menu.js中通过读取该配置文件identity属性对应的值,提供不同的菜单项,代码如下
我在根目录下添加了一个VIP.html文件,在点击新创建的菜单项时打开这个页面
global.json
{
"name":"前端小白",
"color":"black",
"identity":"VIP"
}
VIP.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VIP</title>
</head>
<body>
<p>欢迎金主爸爸</p>
</body>
</html>
menu.js
const { Menu, BrowserWindow, MenuItem, Notification} = require('electron')
const fs = require('fs')
let template = [{
label: '文件',
submenu: [{
label: '新建窗口',
click: () => {
let newWin = new BrowserWindow({
width: 500,
height: 500,
webPreferences: {
nodeIntegration: true
}
})
newWin.loadFile('index2.html')
newWin.on('closed', () => {
newWin = null
})
}
},
{ label: '新建文件' }
]
}, {
label: '编辑',
submenu: [
{ label: '撤销' },
{ label: '恢复' }
]
}]
const menu = Menu.buildFromTemplate(template) // 根据template生成一个Menu对象
const { identity } = JSON.parse(fs.readFileSync('global.json'))
if(identity === "VIP"){
menu.append(new MenuItem({label:'会员点击',click(){
let newWin = new BrowserWindow({
width: 500,
height: 500,
webPreferences: {
nodeIntegration: true
}
})
newWin.loadFile('VIP.html')
newWin.on('closed', () => {
newWin = null
})
}}))
}
Menu.setApplicationMenu(menu) // 根据menu生成菜单
点击之后我们可以看到,打开了一个新的页面
金主爸爸的好感度直线上升,又能继续给我们氪金了
而如果global.json中identity的值不为VIP,那么就不会显示这个菜单项
{
"name":"前端小白",
"color":"black",
"identity":"非VIP"
}
MenuItem的常见属性
实际上上面已经用到了好几个MenuItem的属性,即使是通过使用传入模板然后使用buildFromTemplate的方法,模板中的对象也和传入MenuItem的参数一致,这里说说MenuItem的几个常见的属性
- label:菜单项的内容
- 类型:string
- 可选
- click:点击菜单项时对应的操作
- 类型:function
- 可选
- submenu:子菜单数组,该属性对应于type:submenu,设置了该属性后,type:submenu 可以被忽略,如果设置了其他type,那么submenu无效
- 类型:数组成员为MenuItem的传入对象的数组
- 可选
- type:菜单类型
- 类型:string
- 可选
- 可填的值有
- normal:普通菜单
- separator:分隔线,和label不能一起使用,否则会不显示内容
- submenu:有子菜单的菜单
- checkbox:可选菜单,与checked共用,checked为true时前面有个√
- radio:单选,也对应于checked
- role:指定菜单项的行为,使用click属性后该属性会无效,该属性对应于一些electron默认的行为,详情可见文档
- 类型:string
- 可选
右键菜单的编辑
右键菜单的编辑,其实和顶部菜单的编辑差不多,只是调用了Menu的另一个API:popup
Menu.popup
首先,我们看一下这个API
- 功能:将该Menu实例中添加的菜单项作为 browserWindow 中的上下文菜单弹出,browserWindow为传入的对象的一个属性值
- 参数:对象
- browserWindow: BrowserWindow对象(可选)为要打开上下文菜单的窗口,默认为当前窗口
- x:number(可选)为打开菜单离窗口左边界的距离,默认为鼠标当前的位置,如果设置该值,则必须设置y,否则无效
- y:number(可选)为打开菜单离窗口上边界的距离,默认为鼠标当前的位置,如果设置该值,则必须设置x,否则无效
- positioningItem:number(可选)在鼠标光标下方的指定坐标处定位的菜单项的索引。默认值为-1 在macOS下使用,我没尝试过所以直接用了官方的解释
- callback:function(可选)回调函数,触发操作后的回调
右键打开菜单示例
这里的默认打开窗口,也可以通过romate拿到主线程的getCurrentWindow方法来得到当前窗口对应的BrowserWindow对象
const { remote } = require('electron')
const { BrowserWindow, Menu, MenuItem } = remote
window.addEventListener('load',()=>{
const menu = new Menu() // 创建一个Menu对象
menu.append(new MenuItem({ label: 'MenuItem1', click() { console.log('item 1 clicked') } })) // 添加MenuItem菜单项
window.addEventListener('contextmenu', (e) => {
e.preventDefault()
menu.popup({ window: remote.getCurrentWindow() })
}, false)
})
这里要注意的是,我们需要打开控制台才能看到打印的内容,但是这里因为我们使用了自己的顶部菜单,所以也无法使用ctrl+shift+i来打开控制台,这里需要把控制台添加到我们的菜单中,也可以直接添加在右键点击菜单里
const menu = new Menu() // 创建一个Menu对象
menu.append(new MenuItem({ label: 'MenuItem1', click() { console.log('item 1 clicked') } })) // 添加MenuItem菜单项
menu.append(new MenuItem({ label: '打开开发者工具', role: 'toggledevtools' }))
window.addEventListener('contextmenu', (e) => {
e.preventDefault()
menu.popup({ window: remote.getCurrentWindow() })
}, false)
右键打开控制台,再右键点击MenuItem1就可以看到控制台打印内容了