Electron 菜单、托盘与多窗口

使用应用菜单、托盘和窗口引用,完成常见的桌面端窗口协作。

🎯 引言

这篇文章介绍两个常见功能:点击菜单里的“设置”打开设置窗口,以及点击托盘图标找回已经隐藏的主窗口

这个功能由主进程完成,因为应用菜单和窗口都属于 Electron 的桌面能力,不属于网页里的 DOM。

学完本篇,你将能够:

  • Menu 创建应用菜单。
  • 给菜单项添加 click 点击事件。
  • click 中创建新的 BrowserWindow
  • 理解托盘图标的位置和用途。
macOS 的应用菜单显示在屏幕最上方,不在 Electron 窗口里面。Windows 和部分 Linux 桌面环境的菜单通常显示在窗口顶部。

🧱 准备一个设置页面

先创建 settings.html,它就是设置窗口要显示的页面。

请把 settings.html 放在 main.js 同一级目录:

settings.html
<!doctype html>
<html lang="zh-CN">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="Content-Security-Policy" content="default-src 'self'" />
        <title>设置</title>
    </head>
    <body>
        <h1>设置窗口</h1>
        <p>如果你能看到这个页面,说明菜单已经成功打开了新窗口。</p>
    </body>
</html>

这一步只是准备页面,还不会看到新窗口。


🛠 编写主进程代码

接下来替换 main.js

这段代码运行后的效果是:点击菜单栏里的“文件 → 设置”,打开刚才创建的 settings.html

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

function createMainWindow() {
    const mainWindow = new BrowserWindow({
        width: 800,
        height: 600,
    });

    mainWindow.loadFile(path.join(__dirname, 'index.html'));
}

function createMenu() {
    const template = [];

    if (process.platform === 'darwin') {
        template.push({ role: 'appMenu' });
    }

    template.push({
        label: '文件',
        submenu: [
            {
                label: '设置',
                click: () => {
                    const settingsWindow = new BrowserWindow({
                        width: 520,
                        height: 420,
                    });

                    settingsWindow.loadFile(path.join(__dirname, 'settings.html'));
                },
            },
            { type: 'separator' },
            { role: 'quit' },
        ],
    });

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

app.whenReady().then(() => {
    createMainWindow();
    createMenu();

    app.on('activate', () => {
        if (BrowserWindow.getAllWindows().length === 0) {
            createMainWindow();
        }
    });
});

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

运行项目:

npm start

现在点击“文件 → 设置”,应该会打开一个新的设置窗口。

如果你使用 macOS,请先点击 Electron 窗口,让应用获得焦点,再去屏幕最上方找“文件”菜单。


✨ 菜单代码怎么理解

createMenu 负责创建应用菜单。

先看这几行:

const template = [];

if (process.platform === 'darwin') {
    template.push({ role: 'appMenu' });
}

template 是菜单配置数组。

macOS 有一个特殊规则:第一个菜单通常会显示成应用名称。所以在 macOS 上,我们先放入 { role: 'appMenu' },再添加自己的“文件”菜单。

再看真正的“文件”菜单:

template.push({
    label: '文件',
    submenu: [
        {
            label: '设置',
            click: () => {
                const settingsWindow = new BrowserWindow({
                    width: 520,
                    height: 420,
                });

                settingsWindow.loadFile(path.join(__dirname, 'settings.html'));
            },
        },
        { type: 'separator' },
        { role: 'quit' },
    ],
});

这里的关系很简单:

  • label: '文件':菜单栏上的“文件”。
  • label: '设置':“文件”下面的“设置”菜单项。
  • click:点击“设置”时执行的函数。
  • new BrowserWindow():创建新窗口。
  • loadFile():让新窗口显示 settings.html

也就是说,真正打开设置窗口的是这两行:

const settingsWindow = new BrowserWindow({
    width: 520,
    height: 420,
});

settingsWindow.loadFile(path.join(__dirname, 'settings.html'));

🪤 常见问题

看不到“文件”菜单

如果你是 macOS,菜单在屏幕最上方,不在窗口里面。先点击 Electron 窗口,让应用获得焦点,再查看屏幕顶部菜单栏。

点击“设置”没有反应

先检查 settings.html 是否和 main.js 在同一个目录。还要完全退出 Electron,再重新运行 npm start

每点一次都会打开一个新设置窗口

这是正常现象。本文先讲清楚“菜单点击打开窗口”。如果要限制只打开一个设置窗口,需要额外保存窗口引用,后面再学。

为什么菜单代码要写在主进程里

应用菜单是桌面应用的一部分,不是 HTML 页面里的元素,所以它由主进程创建。


🖥 托盘是什么

Electron 的 Tray 指的是应用放在系统桌面边缘的一个小图标。它不是 macOS 的程序坞,也不是 Windows 任务栏的整体:

系统托盘通常出现的位置它和什么不同
Windows任务栏右侧的通知区域,也叫系统托盘任务栏还负责显示打开的窗口,托盘只是其中一小块区域
macOS屏幕顶部菜单栏右侧的状态区域程序坞负责启动和切换应用,托盘图标负责显示应用菜单或状态
Linux取决于桌面环境,通常是面板上的状态区域不同桌面环境对托盘支持可能不同

托盘适合放置需要在后台运行的应用,例如下载工具、同步工具或聊天工具。用户点击图标后,可以显示主窗口、暂停任务或退出应用。

托盘通常由主进程创建,并保存主窗口引用。下面的例子创建一个托盘菜单,菜单中的“显示窗口”会把已经隐藏的主窗口显示出来:

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

let tray;

function createTray() {
    tray = new Tray(path.join(__dirname, 'assets/tray.png'));
    tray.setContextMenu(
        Menu.buildFromTemplate([
            {
                label: '显示窗口',
                click: () => mainWindow.show(),
            },
            {
                label: '退出',
                role: 'quit',
            },
        ]),
    );
}

窗口、托盘和菜单都由主进程管理。窗口关闭后要同步清理引用,创建多个窗口时也要明确哪个窗口负责哪个功能,避免每次点击菜单都无条件创建新窗口。


🧾 小节总结

  • Electron 应用菜单通过 Menu 创建。
  • Menu.buildFromTemplate 把菜单配置转换成菜单对象。
  • Menu.setApplicationMenu 把菜单安装到应用中。
  • 菜单项的 click 会在用户点击后执行。
  • click 中创建 BrowserWindow,就可以通过菜单打开新窗口。
  • Tray 是桌面边缘的应用状态图标,不等于程序坞或整个任务栏。
  • macOS 的菜单显示在屏幕最上方,不在窗口内部。

❓ 知识问答

Q1:菜单可以写在 index.html 里吗?

A:不可以。本文讲的是桌面应用菜单,不是网页按钮,所以要在主进程中创建。

Q2:click 会自动执行吗?

A:不会。只有用户点击对应菜单项时,click 里的函数才会执行。

Q3:设置窗口为什么要加载 settings.html

A:BrowserWindow 只负责创建窗口,窗口里面显示什么内容,需要通过 loadFile 指定。


🧪 小练习

练习一:把设置窗口尺寸改成 600 × 400

const settingsWindow = new BrowserWindow({
    // 请在这里编写代码
});

练习二:把“设置”菜单项改名为“偏好设置”。

{
    label: '偏好设置',
    click: () => {
        // 保持原来的窗口创建代码
    },
}

🎉 恭喜你已经掌握 Electron 通过菜单打开窗口的基本写法啦!