electron

窗口管理与系统交互

By AI-Writer 11 min read

前言

Electron 的窗口管理能力远超普通浏览器窗口,支持自定义标题栏、透明窗口、全屏 kiosk 模式、多窗口层级关系、系统托盘和全局快捷键。本篇文章将全面讲解这些系统级交互功能,让你的桌面应用具备完整的原生体验。

BrowserWindow 高级配置

自定义标题栏

设置 frame: false 可以隐藏系统窗口边框,实现完全自定义的标题栏:

typescript
// main.ts
const win = new BrowserWindow({
  width: 1200,
  height: 800,
  frame: false,         // 隐藏原生标题栏
  titleBarStyle: 'hidden',
  titleBarOverlay: true, // Windows/Linux 上的窗口控件覆盖层
  // macOS 上可添加 Traffic Light 位置
  titleBarOverlay: {
    color: '#ffffff',
    symbolColor: '#333333',
    height: 40,
  },
});
html
<!-- index.html — 自定义标题栏 -->
<header class="titlebar">
  <div class="titlebar-drag">
    <span class="title">我的 Electron 应用</span>
  </div>
  <div class="window-controls">
    <button id="btn-minimize">&#x2212;</button>
    <button id="btn-maximize">&#x25A1;</button>
    <button id="btn-close">&#x2715;</button>
  </div>
</header>

<style>
  .titlebar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    height: 40px;
    background: #f5f5f5;
    -webkit-app-region: drag; /* 可拖动区域 */
  }
  .window-controls { -webkit-app-region: no-drag; }
  .window-controls button {
    width: 46px;
    height: 40px;
    border: none;
    background: transparent;
    cursor: pointer;
  }
</style>
typescript
// index.html 中的渲染进程脚本
const { ipcRenderer } = require('electron');

document.getElementById('btn-minimize').onclick = () =>
  ipcRenderer.send('window:minimize');
document.getElementById('btn-maximize').onclick = () =>
  ipcRenderer.send('window:maximize');
document.getElementById('btn-close').onclick = () =>
  ipcRenderer.send('window:close');
typescript
// main.ts — IPC 处理器
ipcMain.on('window:minimize', (event) => {
  BrowserWindow.fromWebContents(event.sender)?.minimize();
});

ipcMain.on('window:maximize', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  if (win?.isMaximized()) {
    win.unmaximize();
  } else {
    win?.maximize();
  }
});

ipcMain.on('window:close', (event) => {
  BrowserWindow.fromWebContents(event.sender)?.close();
});

注意:可拖动区域使用 -webkit-app-region: drag,内部按钮等控件需设置为 no-drag,否则无法点击。

透明窗口

typescript
// 透明窗口需要先设置 frame: false
const win = new BrowserWindow({
  width: 400,
  height: 400,
  transparent: true,  // 透明背景
  frame: false,
  resizable: false,
});

win.loadFile('circular-widget.html');
css
/* circular-widget.html */
body {
  background: transparent;
  margin: 0;
}
.widget {
  width: 300px;
  height: 300px;
  background: rgba(0, 0, 0, 0.6);
  border-radius: 50%;
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
}

Kiosk 模式

Kiosk 模式让应用全屏运行并禁止退出,常用于展厅、ATM、自助终端:

typescript
const win = new BrowserWindow({
  kiosk: true,
  // kiosk: false 模式下可添加退出快捷键
});

// 退出 kiosk 模式
if (win.isKiosk()) {
  win.setKiosk(false);
}

多窗口管理

父子窗口

子窗口永远显示在父窗口之上,父窗口关闭时子窗口也会关闭:

typescript
function createMainWindow() {
  const main = new BrowserWindow({ width: 1200, height: 800 });
  main.loadFile('main.html');
  return main;
}

function openSettings(parent: BrowserWindow) {
  const settings = new BrowserWindow({
    width: 500,
    height: 400,
    parent,           // 关联父窗口
    modal: true,      // 模态窗口,父窗口被阻塞
    show: false,
  });
  settings.loadFile('settings.html');
  settings.once('ready-to-show', () => settings.show());
}

窗口组

macOS 支持将多个窗口编组,关闭一个窗口时整组同时关闭:

typescript
import { app } from 'electron';

const win1 = new BrowserWindow();
const win2 = new BrowserWindow();

// macOS:将窗口加入同一 app 窗口组
app.addWindowsToAccessibilityGroup([win1, win2]);

BrowserView

BrowserView 是一种轻量级的多窗口方案,将内容嵌入到主窗口的特定区域,而不是独立的 OS 窗口:

typescript
// main.ts — 使用 BrowserView
import { BrowserView, BrowserWindow } from 'electron';

const mainWindow = new BrowserWindow({ width: 1200, height: 800 });
const view = new BrowserView({
  webPreferences: {
    nodeIntegration: false,
    contextIsolation: true,
  },
});

// 将 view 附加到主窗口
mainWindow.setBrowserView(view);
view.setBounds({ x: 0, y: 50, width: 1200, height: 750 });
view.webContents.loadURL('https://example.com');

// 主窗口标题栏区域由原生渲染
mainWindow.loadFile('index.html');

系统托盘

创建托盘图标

typescript
// main.ts
import { Tray, Menu, nativeImage } from 'electron';
import path from 'path';

let tray: Tray;

function createTray() {
  // 创建托盘图标
  const iconPath = path.join(__dirname, '../resources/icon.png');
  const icon = nativeImage.createFromPath(iconPath);

  // macOS:建议使用 Template Image(会自动适配亮/暗模式)
  const trayIcon = icon.resize({ width: 16, height: 16 });

  tray = new Tray(trayIcon);
  tray.setToolTip('My Electron App');

  // 右键菜单
  const contextMenu = Menu.buildFromTemplate([
    {
      label: '显示窗口',
      click: () => {
        const win = BrowserWindow.getAllWindows()[0];
        win?.show();
        win?.focus();
      },
    },
    { type: 'separator' },
    {
      label: '设置',
      accelerator: 'CmdOrCtrl+,',
      click: () => openSettings(),
    },
    { type: 'separator' },
    {
      label: '退出',
      accelerator: 'CmdOrCtrl+Q',
      click: () => app.quit(),
    },
  ]);

  tray.setContextMenu(contextMenu);

  // 点击托盘图标
  tray.on('click', () => {
    const win = BrowserWindow.getAllWindows()[0];
    win?.show();
    win?.focus();
  });

  // macOS:左键点击事件
  tray.on('click', () => tray.popUpContextMenu());
}

托盘菜单动态更新

typescript
// 动态更新托盘菜单
function updateTrayMenu(count: number) {
  const newMenu = Menu.buildFromTemplate([
    {
      label: `未读消息: ${count}`,
      enabled: false,
    },
    { type: 'separator' },
    {
      label: '打开应用',
      click: () => mainWindow.show(),
    },
    { label: '退出', click: () => app.quit() },
  ]);
  tray.setContextMenu(newMenu);
}

全局快捷键

globalShortcut 在应用未聚焦时也能响应,适合音乐播放器(全局播放暂停)、截图工具等场景:

typescript
// main.ts
import { globalShortcut } from 'electron';

app.whenReady().then(() => {
  // 注册全局快捷键
  globalShortcut.register('MediaPlayPause', () => {
    mainWindow.webContents.send('media:play-pause');
  });

  globalShortcut.register('CmdOrCtrl+Shift+S', () => {
    mainWindow.webContents.send('app:screenshot');
  });

  // 检查快捷键是否注册成功
  const isRegistered = globalShortcut.isRegistered('MediaPlayPause');
  console.log('MediaPlayPause 已注册:', isRegistered);
});

app.on('will-quit', () => {
  // 应用退出前注销所有全局快捷键
  globalShortcut.unregisterAll();
});

在渲染进程中使用

typescript
// preload.ts
contextBridge.exposeInMainWorld('electronAPI', {
  onMediaPlayPause: (cb: () => void) => {
    ipcRenderer.on('media:play-pause', cb);
    return () => ipcRenderer.removeListener('media:play-pause', cb);
  },
  onScreenshot: (cb: () => void) => {
    ipcRenderer.on('app:screenshot', cb);
    return () => ipcRenderer.removeListener('app:screenshot', cb);
  },
});

屏幕信息获取

typescript
import { screen } from 'electron';

// 获取所有屏幕
const displays = screen.getAllDisplays();
console.log('屏幕数量:', displays.length);

// 获取当前鼠标所在屏幕
const currentDisplay = screen.getDisplayNearestPoint(
  screen.getCursorScreenPoint()
);
console.log('当前屏幕分辨率:', currentDisplay.size);

// 获取主屏幕的工作区域(排除系统任务栏)
const workArea = screen.getPrimaryDisplay().workArea;
console.log('可用区域:', workArea);

窗口状态持久化

应用关闭时保存窗口位置和大小,下次启动时恢复:

typescript
import { app } from 'electron';
import fs from 'fs';
import path from 'path';

const configPath = path.join(app.getPath('userData'), 'window-state.json');

interface WindowState {
  x?: number;
  y?: number;
  width: number;
  height: number;
  isMaximized: boolean;
}

function loadWindowState(): WindowState {
  try {
    return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
  } catch {
    return { width: 1200, height: 800, isMaximized: false };
  }
}

function saveWindowState(win: BrowserWindow) {
  const bounds = win.getBounds();
  const state: WindowState = {
    ...bounds,
    isMaximized: win.isMaximized(),
  };
  fs.writeFileSync(configPath, JSON.stringify(state));
}

app.whenReady().then(() => {
  const savedState = loadWindowState();
  const win = new BrowserWindow({
    ...savedState,
    webPreferences: { nodeIntegration: false, contextIsolation: true },
  });

  if (savedState.isMaximized) win.maximize();

  win.on('close', () => saveWindowState(win));
});

小结

  • frame: false 隐藏系统标题栏,配合 -webkit-app-region 实现自定义拖动区域
  • transparent: true 创建透明窗口,适合桌面小部件
  • BrowserView 是轻量级多区域内容嵌入方案
  • 系统托盘通过 Tray + Menu 实现后台常驻和快捷操作
  • globalShortcut 注册全局快捷键,即使应用未聚焦也能响应
  • 窗口状态持久化提升用户体验,下次启动恢复上次的窗口位置和大小

下一篇文章我们将介绍 原生对话框与文件操作,包括文件选择器、文件夹操作和 Shell 模块的使用。

#electron #BrowserWindow #Tray #globalShortcut #多窗口

评论

A

Written by

AI-Writer

Related Articles

electron
#3

IPC 通信机制详解

深入理解 Electron 的 IPC 模块:invoke/handle 双向异步模式、ipcRenderer.on 事件监听、channel 命名规范、进程间数据传输规则

Read More
electron
#1

Electron 入门与核心架构

了解 Electron 的双引擎架构(Chromium + Node.js)、主进程与渲染进程的职责分工,快速搭建第一个 Hello World 应用,掌握 BrowserWindow 基本配置

Read More
electron
#5

窗口管理与系统交互

掌握 BrowserWindow 高级配置(frame、transparent、kiosk)、多窗口管理、系统托盘 Tray、全局快捷键 globalShortcut,以及屏幕与窗口状态监控

Read More