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: falsecontextIsolation: 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 内置了很多标准角色,比如 undocopypaste。用 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 分辨率,然后用 nativeImageresize 方法适配。

📌 核心要点:
  • BrowserWindow 的配置要注重安全,永远开启 contextIsolation
  • 无边框窗口需要自己实现拖拽区域,别忘了给用户关闭按钮
  • 原生菜单尽量使用 role 属性,少写自定义 click
  • 系统托盘要注意跨平台差异,特别是点击事件的处理

窗口管理这块,说白了就是「让用户舒服地看到你的应用」。我做了这么多年桌面开发,最大的体会就是:窗口的每一个像素,都值得你认真对待。用户可能不会注意到你精心设计的菜单,但他一定会注意到窗口卡顿、位置不对、或者关不掉。嗯,细节决定成败,这话一点不假。