3、窗口管理与界面布局:BrowserWindow的创建与配置;窗口大小、位置、无边框窗口;使用Menu模块创建原生菜单;Tray(系统托盘)的实现。
窗口管理,说白了就是 Electron 应用的「脸面」。用户第一眼看到的就是这个窗口。我刚开始做桌面应用时,总觉得窗口嘛,能打开就行。直到有一次,产品经理跟我说:「这个窗口能不能在屏幕正中间打开?还有,关闭按钮能不能隐藏?」嗯,从那以后我才意识到,窗口管理远不止 new BrowserWindow() 这么简单。
3.1 BrowserWindow 的创建与基础配置
创建一个窗口,代码其实就一行。但真正用好它,你得懂里面的门道。
const { BrowserWindow } = require('electron')
const win = new BrowserWindow({
width: 1024,
height: 768,
title: '我的应用',
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
这里有个坑,我必须要说。很多新手喜欢把 nodeIntegration 设为 true,图省事。但我建议你千万别这么干。为什么?因为安全。你想想看,如果渲染进程能直接调用 Node.js API,那随便一个 XSS 漏洞就能让攻击者控制你的电脑。我个人习惯是:永远开启 contextIsolation,用 preload 脚本做桥接。
nodeIntegration: false 和 contextIsolation: true。这是 Electron 官方推荐的安全基线。
3.2 窗口大小、位置与无边框窗口
窗口的位置和大小,直接决定了用户体验。我记得有一次做视频会议软件,窗口必须在屏幕正中间弹出,而且不能太小。
控制窗口大小
const win = new BrowserWindow({
width: 800,
height: 600,
minWidth: 400,
minHeight: 300,
maxWidth: 1600,
maxHeight: 1200,
resizable: true // 是否允许用户调整大小
})
这里有个小技巧:如果你想让窗口启动时最大化,可以设置 show: false,然后等窗口准备好后再调用 maximize()。这样用户就不会看到窗口从默认大小变到最大化的「动画」了。
控制窗口位置
让窗口居中显示,是我最常用的功能。Electron 提供了现成的方法:
win.center() // 直接居中
// 或者手动计算屏幕中心
const { screen } = require('electron')
const primaryDisplay = screen.getPrimaryDisplay()
const { width: screenWidth, height: screenHeight } = primaryDisplay.workAreaSize
win.setPosition(
Math.round((screenWidth - 800) / 2),
Math.round((screenHeight - 600) / 2)
)
我个人更推荐用 center() 方法,省事。但如果你需要精确控制,比如让窗口出现在屏幕的右下角(像系统通知那样),那就得手动计算了。
无边框窗口
无边框窗口,说白了就是去掉系统自带的标题栏和边框。这在做自定义标题栏、播放器、或者游戏时特别有用。
const win = new BrowserWindow({
width: 800,
height: 600,
frame: false, // 无边框
transparent: true, // 透明背景(可选)
titleBarStyle: 'hidden' // macOS 上隐藏标题栏但保留窗口控制按钮
})
嗯,这里要注意。无边框窗口虽然好看,但有个大问题:用户没法拖动窗口了。你需要自己在 HTML 里实现拖拽功能。我一般会在 CSS 里加一个可拖拽区域:
/* 在渲染进程的 CSS 中 */
.drag-region {
-webkit-app-region: drag;
height: 40px;
background: #2c3e50;
}
.no-drag {
-webkit-app-region: no-drag;
}
然后在 HTML 里:
<div class="drag-region">
<span class="no-drag">点击这里不会触发拖拽</span>
</div>
3.3 使用 Menu 模块创建原生菜单
原生菜单,就是应用顶部那个菜单栏。在 Windows 上它在窗口内部,在 macOS 上它在屏幕顶部。Electron 的 Menu 模块可以让你完全自定义它。
const { Menu, app } = require('electron')
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => { console.log('新建文件') }
},
{ type: 'separator' },
{
label: '退出',
accelerator: 'CmdOrCtrl+Q',
click: () => { app.quit() }
}
]
},
{
label: '编辑',
submenu: [
{ label: '撤销', accelerator: 'CmdOrCtrl+Z', role: 'undo' },
{ label: '重做', accelerator: 'Shift+CmdOrCtrl+Z', role: 'redo' },
{ type: 'separator' },
{ label: '剪切', accelerator: 'CmdOrCtrl+X', role: 'cut' },
{ label: '复制', accelerator: 'CmdOrCtrl+C', role: 'copy' },
{ label: '粘贴', accelerator: 'CmdOrCtrl+V', role: 'paste' }
]
}
]
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
这里有个细节:role 属性。Electron 内置了很多标准角色,比如 undo、copy、paste。用 role 的好处是,Electron 会自动处理这些操作,你不需要自己写 click 回调。我建议你尽量用 role,少写自定义 click,这样代码更干净。
另外,macOS 上有个特殊要求:第一个菜单必须是应用名菜单。Electron 会自动处理这个,但如果你自定义了菜单,记得把 app 菜单放在第一位:
if (process.platform === 'darwin') {
template.unshift({
label: app.getName(),
submenu: [
{ role: 'about' },
{ type: 'separator' },
{ role: 'quit' }
]
})
}
3.4 Tray(系统托盘)的实现
系统托盘,就是任务栏右下角(macOS 上是菜单栏右侧)的那个小图标。很多应用用它来常驻后台,比如微信、QQ。
const { Tray, Menu, nativeImage } = require('electron')
let tray = null
app.whenReady().then(() => {
const icon = nativeImage.createFromPath('path/to/icon.png')
tray = new Tray(icon)
const contextMenu = Menu.buildFromTemplate([
{
label: '显示窗口',
click: () => { mainWindow.show() }
},
{
label: '退出',
click: () => { app.quit() }
}
])
tray.setToolTip('我的应用')
tray.setContextMenu(contextMenu)
// 点击托盘图标显示窗口
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
})
这里有个坑,我必须要提醒你。在 Windows 上,托盘图标点击事件是 click,但在 macOS 上,点击事件是 left-click。而且 macOS 上默认是右键弹出菜单。所以如果你想让跨平台行为一致,最好这样处理:
tray.on('click', (event, bounds) => {
// Windows 和 Linux 上直接处理
if (process.platform !== 'darwin') {
mainWindow.show()
}
})
// macOS 上单独处理
if (process.platform === 'darwin') {
tray.on('left-click', () => {
mainWindow.show()
})
}
另外,托盘图标的大小也有讲究。Windows 上推荐 16x16 或 32x32 像素,macOS 上推荐 22x22 像素(Retina 屏用 44x44)。我一般会准备两套图标,一套普通分辨率,一套 2x 分辨率,然后用 nativeImage 的 resize 方法适配。
- BrowserWindow 的配置要注重安全,永远开启 contextIsolation
- 无边框窗口需要自己实现拖拽区域,别忘了给用户关闭按钮
- 原生菜单尽量使用 role 属性,少写自定义 click
- 系统托盘要注意跨平台差异,特别是点击事件的处理
窗口管理这块,说白了就是「让用户舒服地看到你的应用」。我做了这么多年桌面开发,最大的体会就是:窗口的每一个像素,都值得你认真对待。用户可能不会注意到你精心设计的菜单,但他一定会注意到窗口卡顿、位置不对、或者关不掉。嗯,细节决定成败,这话一点不假。