Electron桌面应用开发:自定义菜单

完成初始应用的创建Electron桌面应用开发:创建应用,随后我们就可以自定义软件的菜单了。菜单可以帮助用户快速找到和执行命令,而不需要记住复杂的快捷键,通过将相关功能组织在一起,用户可以更容易地发现和使用应用程序的各种特性。同时菜单允许开发者提供更多的功能选项而不必担心界面会因此变得拥挤或难以导航,比如下拉菜单、弹出菜单等可以在有限的空间内提供大量的选项。
Electron的原始菜单为以下页面:

取消顶部菜单显示

这里可以使用两种常用的方法,第一种是在窗口创建函数中设置frame: false:

javascript 复制代码
const {app, BrowserWindow, Menu} = require('electron');

let win = null;

function createWindow() {
    win = new BrowserWindow({
        width: 800,
        height: 600,
        frame: false,
        webPreferences: {
            nodeIntegration: true
        },
    });

    win.loadFile('index.html');

    win.on('closed', () => {
        win = null;
    });
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

这样大小化和关闭按钮、标题也全部消失了,所以只适合个别情况使用。

第二种是在创建窗口函数中加入Menu.setApplicationMenu(null);设置,这样可以保留标题等内容:

javascript 复制代码
const {app, BrowserWindow, Menu} = require('electron');

let win = null;

function createWindow() {
    win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        },
    });

    win.loadFile('index.html');
    Menu.setApplicationMenu(null);

    win.on('closed', () => {
        win = null;
    });
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

自定义子菜单

这里我们可以在主文件main.js中直接添加自定义菜单的代码,也可以新建一个menu.js文件,随后在main.js中进行引用(个人推荐做法)

在Electron中定义菜单需要先引入Menu:

javascript 复制代码
const {Menu} = require('electron')

定义格式如下:

javascript 复制代码
    const template = [
        {
            label: '菜单1',
            submenu: [
                {
                    label: '子菜单1'
                },
                {
                    label: '子菜单2',
                }
            ]
        },

        {
            label: '菜单2',
            submenu: [
                {
                    label: '子菜单1'
                },
                {
                    label: '子菜单2',
                    submenu: [
                        {
                            label: '孙子菜单1'
                        },
                        {
                            label: '孙子菜单2'
                        }
                    ]
                }
            ]
        }
    ]

可以使用click来监听事件,例如:

javascript 复制代码
            {
                label: '欧耶',
                accelerator: 'CmdOrCtrl+O',
                click: () => {
                    console.log('菜单被点击了');
                }
            },

在main.js中直接添加菜单代码的格式如下:

javascript 复制代码
const { app, BrowserWindow, Menu } = require('electron');

let win = null;

function createWindow() {
    win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        },
    });

    win.loadFile('index.html');

    win.on('closed', () => {
        win = null;
    });
}

const template = [
    {
        label: '菜单1',
        submenu: [
            {
                label: '子菜单1'
            },
            {
                label: '子菜单2',
            }
        ]
    },

    {
        label: '菜单2',
        submenu: [
            {
                label: '子菜单1'
            },
            {
                label: '子菜单2',
                submenu: [
                    {
                        label: '孙菜单1'
                    },
                    {
                        label: '孙菜单2'
                    }
                ]
            }
        ]
    },

    {
        label: '帮助',
        role: 'help',
        click() { require('electron').shell.openExternal('https://example.com/help') }
    }
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

app.on('ready', createWindow);

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', () => {
    if (win === null) {
        createWindow();
    }
});

这里我们还是推荐使用将创建菜单的代码转移到其他文件中,比如新建一个menu.js,这样可以更加方便的进行代码编写和问题排查:
main.js:

javascript 复制代码
const {app, BrowserWindow, Menu} = require('electron');
// 引入menu.js文件
const menuTemplate = require('./menu.js'); 

let win = null;

function createWindow() {
    win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true
        },
    });

    win.loadFile('index.html');

    win.on('closed', () => {
        win = null;
    });

	// 创建窗口
    const menu = Menu.buildFromTemplate(menuTemplate(win));
    Menu.setApplicationMenu(menu);
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

menu.js:

javascript 复制代码
const {Menu} = require('electron')

module.exports = function (win) {
    const template = [
        {
            label: '菜单1',
            submenu: [
                {
                    label: '子菜单1'
                },
                {
                    label: '子菜单2',
                }
            ]
        },

        {
            label: '菜单2',
            submenu: [
                {
                    label: '子菜单1'
                },
                {
                    label: '子菜单2',
                    submenu: [
                        {
                            label: '孙菜单1'
                        },
                        {
                            label: '孙菜单2'
                        }
                    ]
                }
            ]
        },

        {
            label: '帮助',
            role: 'help',
            click() { require('electron').shell.openExternal('https://example.com/help') }
        }
    ]
    return template;
}

两种方法的最终效果如下:

上下文菜单

上下文菜单通常在用户右键点击某个元素时显示,通常通过监听事件来创建和显示上下文菜单。

在主文件中需要导入ipcMain模块,main.js:

javascript 复制代码
const { app, BrowserWindow, Menu, ipcMain } = require('electron');
const menuTemplate = require('./menu.js');

let win = null;

function createWindow() {
    win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false,
            preload: __dirname + './preload.js'
        },
    });

    win.loadFile('index.html');

    const menu = Menu.buildFromTemplate(menuTemplate(win));
    Menu.setApplicationMenu(menu);

    win.on('closed', () => {
        win = null;
    });
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', () => {
    if (win === null) {
        createWindow();
    }
});

renderer.js,DOM内容加载完成后执行的逻辑:

javascript 复制代码
document.addEventListener('DOMContentLoaded', () => {
});

使用preload脚本处理上下文菜单的preload.js, 用于安全地暴露API给渲染进程:

javascript 复制代码
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
    send: (channel, data) => {
         // 向主进程发送消息
        ipcRenderer.send(channel, data);
    },
    receive: (channel, func) => {
        // 接收来自主进程的消息
        ipcRenderer.on(channel, (event, ...args) => func(...args)); 
    }
});

menu.js:

javascript 复制代码
const {Menu} = require('electron')

module.exports = function (win) {
    const template = [
        {
            label: '菜单1',
            submenu: [
                {
                    label: '子菜单1'
                },
                {
                    label: '子菜单2',
                }
            ]
        },

        {
            label: '菜单2',
            submenu: [
                {
                    label: '子菜单1'
                },
                {
                    label: '子菜单2',
                    submenu: [
                        {
                            label: '孙菜单1'
                        },
                        {
                            label: '孙菜单2'
                        }
                    ]
                }
            ]
        },

        {
            label: '帮助',
            role: 'help',
            click() { require('electron').shell.openExternal('https://example.com/help') }
        }
    ]
    return template;
}

index.html:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./style.css">
    <title>GGBond勇猛无敌</title>
</head>
<body>
    <h1>Hello GGBond</h1>

    <script src="./renderer.js"></script>
</body>
</html>

弹出式菜单

弹出式菜单可以通过编程方式手动显示,不需要特定的触发事件。
main.js:

javascript 复制代码
const { app, BrowserWindow, Menu } = require('electron');
const menuTemplate = require('./menu.js'); // 引入自定义菜单模板

let win = null;

function createWindow() {
    win = new BrowserWindow({
        width: 800,
        height: 600,
        webPreferences: {
            nodeIntegration: true,
            contextIsolation: false, // 在使用nodeIntegration时需要禁用contextIsolation
            preload: __dirname + '/preload.js' // 使用preload脚本处理上下文菜单
        },
    });

    win.loadFile('index.html');

    win.on('closed', () => {
        win = null;
    });

    const menu = Menu.buildFromTemplate(menuTemplate(win));
    Menu.setApplicationMenu(menu); // 设置为应用菜单
}

app.on('ready', createWindow);

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit();
    }
});

app.on('activate', () => {
    if (win === null) {
        createWindow();
    }
});

使用preload脚本处理上下文菜单的preload.js,使用contextBridge来安全地暴露API给渲染进程,允许渲染进程调用showPopupMenu方法。:

javascript 复制代码
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
    showPopupMenu: (x, y) => {
        ipcRenderer.invoke('show-popup-menu', x, y);
    }
});

renderer.js:

javascript 复制代码
document.addEventListener('DOMContentLoaded', () => {
    document.body.addEventListener('click', (event) => {
        window.electronAPI.showPopupMenu(event.x, event.y);
    });
});

menu.js:

javascript 复制代码
const {Menu} = require('electron')

module.exports = function (win) {
    const template = [
        {
            label: '菜单1',
            submenu: [
                {
                    label: '子菜单1'
                },
                {
                    label: '子菜单2',
                }
            ]
        },

        {
            label: '菜单2',
            submenu: [
                {
                    label: '子菜单1'
                },
                {
                    label: '子菜单2',
                    submenu: [
                        {
                            label: '孙菜单1'
                        },
                        {
                            label: '孙菜单2'
                        }
                    ]
                }
            ]
        },

        {
            label: '帮助',
            role: 'help',
            click() { require('electron').shell.openExternal('https://example.com/help') }
        }
    ]
    return template;
}

index.html:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="./style.css">
    <title>GGBond勇猛无敌</title>
</head>
<body>
    <h1>Hello GGBond</h1>
    <script src="./src/renderer.js"></script>
</body>
</html>
相关推荐
白帽攻防录7 小时前
SRC 挖洞:PostScript 打印机驱动双 RCE,打印服务器怎么变成内网跳板
开发语言·网络安全·php·内网安全
十年Java程序媛7 小时前
Java枚举Enum完整教程|替代静态常量,开发与面试要点
java·开发语言·spring boot·后端·面试
2603_965148117 小时前
电商选品调研:通过关键词搜索API获取市场热销商品,结合商品详情API分析竞品卖点与定价
开发语言·数据库·人工智能·python·信息可视化
lpfasd1237 小时前
node-sass 停止维护!老项目迁移 Dart Sass 完整教程
前端·rust·sass
xixiaoyunya8 小时前
Python 异步编程入门:用 asyncio 轻松实现并发任务
开发语言·python·php
略懂而已8 小时前
腾讯云COS 自动同步 for windows
windows·云计算·腾讯云
学逆向的8 小时前
C 循环结构的执行效率分析
c语言·开发语言·数据结构·网络安全
小坏讲微服务9 小时前
刚发布一个月的JDK 27,为什么我建议你立刻升级?
java·开发语言·spring boot·spring·架构·求职招聘·springboot4
Data analyse45610 小时前
rnweb 回放数据量太大怎么办?分片存储与断点续传的实现思路
前端·网络·算法·数据分析
xiaogai_gai10 小时前
金蝶收料通知单查询策略实战教程:从源系统拉取到中间层落地的完整路径
java·前端·数据库