人人都会AI编程

10.3 窗口拖拽、缩放、无边框窗口交互实现

更新时间:2026-07-11

在 Electron 中,如果你希望应用有一个完全自定义的标题栏,或者一个没有系统默认窗口装饰的界面,无边框窗口是必选项。但移除了系统提供的标题栏后,窗口的拖拽、缩放、关闭等基础交互就需要开发者自己来实现。本节会完整覆盖从创建无边框窗口到实现自定义标题栏、拖拽移动、边缘缩放、双击全屏等全套交互的实用方法。

10.3.1 创建无边框窗口

在主进程创建窗口时,将 frame 设为 false 即可得到一个无边框窗口:

// main.js (主进程)
const { BrowserWindow } = require('electron');

const win = new BrowserWindow({
  width: 1000,
  height: 700,
  frame: false,          // 移除系统标题栏与窗口边框
  // 保留一个细微的阴影和圆角(仅 macOS 有效,Windows 上可以使用透明窗口 + CSS 模拟)
  transparent: true,     // 按需开启,如需自绘圆角/阴影
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true,
    nodeIntegration: false,
  },
});

在 Windows 上,完全无框通常会让窗口失去阴影和调节大小的区域,因此很多应用会保留一个很细的 titleBarStyle: 'hidden' 或通过 CSS 自己画阴影。如果你不想完全透明,可以保持 transparent: false,并用 CSS 的 border-radiusbox-shadow 模拟外观。

10.3.2 自定义标题栏与窗口控制

在渲染进程的 HTML/CSS 中构建自己的标题栏区域。标题栏通常包含:

  • 应用图标和名称(可拖拽区域)
  • 菜单按钮(可选)
  • 最小化、最大化/还原、关闭按钮
<!-- index.html -->
<div id="title-bar">
  <div class="drag-region">
    <span class="title">我的应用</span>
  </div>
  <div class="window-controls">
    <button id="minimize-btn" title="最小化">─</button>
    <button id="maximize-btn" title="最大化">☐</button>
    <button id="close-btn" title="关闭">✕</button>
  </div>
</div>
<div id="app-content">
  <!-- 你的界面内容 -->
</div>
/* styles.css */
body {
  margin: 0;
  overflow: hidden; /* 避免出现浏览器滚动条干扰自定义拖拽区 */
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

#title-bar {
  display: flex;
  align-items: center;
  height: 32px;
  background: #2c2c2c;
  color: #fff;
  user-select: none;
}

.drag-region {
  flex: 1;
  -webkit-app-region: drag; /* 关键:允许拖拽窗口 */
  padding-left: 12px;
}

/* 按钮区不可拖拽,否则无法点击 */
.window-controls {
  -webkit-app-region: no-drag;
  display: flex;
}

.window-controls button {
  width: 46px;
  height: 32px;
  border: none;
  background: transparent;
  color: #ccc;
  font-size: 14px;
  cursor: pointer;
  transition: background 0.2s;
}

.window-controls button:hover {
  background: rgba(255, 255, 255, 0.1);
}

#close-btn:hover {
  background: #e81123;
  color: #fff;
}

核心是 -webkit-app-region: drag,它告诉 Chromium 这个区域可以被拖拽来移动窗口。按钮所在的 .window-controls 被设置成 no-drag 来恢复正常的鼠标事件,否则按钮将无法点击。

10.3.3 通过预加载脚本暴露窗口控制 IPC

因为渲染进程是在沙箱中运行且没有直接访问 BrowserWindow 的能力,所以需要用 preload.js 暴露安全的 API,然后通过 IPC 让主进程执行最小化、最大化和关闭操作。

preload.js

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('windowAPI', {
  minimize: () => ipcRenderer.send('window-minimize'),
  maximize: () => ipcRenderer.send('window-maximize'),
  close: () => ipcRenderer.send('window-close'),
});

主进程 main.js 中接收事件

const { ipcMain } = require('electron');

ipcMain.on('window-minimize', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  win.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) => {
  const win = BrowserWindow.fromWebContents(event.sender);
  win.close();
});

渲染进程调用

// renderer.js
document.getElementById('minimize-btn').addEventListener('click', () => {
  window.windowAPI.minimize();
});

document.getElementById('maximize-btn').addEventListener('click', () => {
  window.windowAPI.maximize();
});

document.getElementById('close-btn').addEventListener('click', () => {
  window.windowAPI.close();
});

10.3.4 监听窗口状态,同步最大化/还原按钮图标

当用户双击标题栏或者从系统任务栏最大化窗口时,我们需要更新按钮的样式。主进程可监听窗口的 maximizeunmaximize 事件并通知渲染进程。

主进程

win.on('maximize', () => {
  win.webContents.send('window-state-changed', 'maximized');
});
win.on('unmaximize', () => {
  win.webContents.send('window-state-changed', 'normal');
});

preload.js 增加监听方法

contextBridge.exposeInMainWorld('windowAPI', {
  // ... 上面的三个方法
  onWindowStateChanged: (callback) => {
    ipcRenderer.on('window-state-changed', (event, state) => callback(state));
  },
});

渲染进程

window.windowAPI.onWindowStateChanged((state) => {
  const btn = document.getElementById('maximize-btn');
  if (state === 'maximized') {
    btn.textContent = '❐'; // 还原图标
    btn.title = '还原';
  } else {
    btn.textContent = '☐';
    btn.title = '最大化';
  }
});

10.3.5 窗口拖拽移动与双击最大化

因为我们已经在标题栏设置了 -webkit-app-region: drag,窗口拖拽已经自动生效。不过 Chrome 中的拖拽区域默认会取消 dblclick 事件,所以如果想实现双击标题栏最大化,需要额外处理。

方案一:在预加载脚本中暴露一个“最大化切换”功能,并让渲染进程双击时调用(同上)。但是 drag 区域的双击会被浏览器吞掉,所以可以采用以下 workaround:在拖拽区域的某个不可拖拽的子元素上监听双击。比如:

<div class="drag-region">
  <span class="title" style="-webkit-app-region: no-drag; pointer-events: auto;">我的应用</span>
</div>

然后为 .title 添加双击事件:

document.querySelector('.title').addEventListener('dblclick', () => {
  window.windowAPI.maximize();
});

如果不希望这样,还可以用更底层的方法:在 BrowserWindow 构造时使用 doubleClickable: true(实际上这个选项不存在),更好的思路是利用 webContentsbefore-input-event 或直接在主进程用系统钩子。通常上述方案已经满足需求。

10.3.6 窗口大小调整:边缘拖拽与自定义缩放

无边框窗口默认仍然保留了系统级的窗口边缘(大约几个像素)用于调整大小。可以通过设置 resizable: true(默认值)来保留。但如果你想要更明显的自定义调整手柄,比如在窗口内部某个位置放一个角标用于缩放,需要组合 CSS 和窗口消息。

要禁用系统默认的边缘并完全自绘缩放区域,可以在创建窗口时设置 resizable: false,然后通过在内容区域边缘放置透明的、不可拖拽但可交互的元素,并配合 setBounds 实现类似效果,但实现复杂度较高。多数无边框应用会保留系统原有的调整尺寸边缘,只是自定义标题栏。

如果想在某些区域禁用系统调整(例如标题栏靠近顶部边缘),可以把 -webkit-app-region: drag 铺满整个窗口顶部,这样鼠标移到上方边缘就无法触发垂直调整——这在 macOS 下尤其有用。

10.3.7 平台兼容性与细节微调

  • Windows:移除系统框后,窗口阴影会丢失,可使用第三方库如 electron-window-controls 或通过 CSS box-shadow 加上 transparent: true 模拟阴影。另外需要留意 WS_CAPTION 等样式,但 Electron 已处理大部分。
  • macOS:无边框窗口的“红绿灯”按钮可以通过 titleBarStyle: 'customButtonsOnHover' 等选项保留或隐藏。完全自定义时,通常隐藏它们。
  • Linux:不同桌面环境对于无边框窗口的支持不一致,测试时建议覆盖 GNOME、KDE 等主流环境。

综合以上,一个完整的自定义窗口交互方案,核心在于正确使用 -webkit-app-region、通过 IPC 安全调用窗口操作,并监听状态变化保持 UI 同步。这些技术构成了自定义标题栏、窗口拖拽和基础窗口管理的基础,能够让你的 Electron 应用脱离千篇一律的原生标题栏,形成自己独特的视觉风格。