{T}

无边框窗口

无边框窗口(Frameless Window)是没有默认标题栏和边框的窗口,适用于需要自定义窗口样式的应用,如音乐播放器、效率工具等。

系统架构

无边框窗口结构

code
┌─────────────────────────────────────────────────────────────┐
│                     自定义标题栏 (Custom Title Bar)           │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ [-webkit-app-region: drag]                              ││
│  │  应用名称         自定义内容         窗口控制按钮          ││
│  └─────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                     应用内容区域                              │
│                  (Web Content Area)                         │
│                                                             │
│                                                             │
│                                                             │
│                                                             │
│                                                             │
├─────────────────────────────────────────────────────────────┤
│  ┌─────────────────────────────────────────────────────┐   │
│  │                                           [≡]      │   │ ← 缩放手柄
│  └─────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────┘

与原生窗口对比

特性原生窗口无边框窗口
标题栏系统原生样式完全自定义
窗口控制按钮系统提供需自行实现
拖拽功能系统原生CSS 或自定义实现
阴影效果系统原生部分支持/需模拟
圆角支持受限完全支持
开发成本中高

创建无边框窗口

基本创建

javascript
const { BrowserWindow } = require('electron')

const win = new BrowserWindow({
  width: 800,
  height: 600,
  frame: false,        // 关键:禁用默认边框
  transparent: true,   // 可选:透明背景
  resizable: true      // 可选:仍可调整大小
})

窗口选项详解

javascript
const win = new BrowserWindow({
  width: 1000,
  height: 700,
  
  // 核心选项
  frame: false,              // 移除原生边框和标题栏
  
  // 外观选项
  transparent: true,         // 允许透明背景
  backgroundColor: '#00000000', // 透明背景色
  hasShadow: true,           // macOS 窗口阴影
  opacity: 0.95,             // 窗口透明度(Windows/macOS)
  
  // 交互选项
  resizable: true,           // 允许调整大小
  movable: true,             // 允许移动
  minimizable: true,         // 允许最小化
  maximizable: true,         // 允许最大化
  closable: true,            // 允许关闭
  
  // macOS 特有选项
  titleBarStyle: 'hidden',   // 保留红绿灯按钮
  trafficLightPosition: { x: 15, y: 15 }, // 红绿灯位置
  vibrancy: 'under-window',  // 毛玻璃效果
  
  // Windows 特有选项
  autoHideMenuBar: true,     // 自动隐藏菜单栏
  
  webPreferences: {
    preload: path.join(__dirname, 'preload.js'),
    contextIsolation: true
  }
})

自定义标题栏

基本结构

html
<!DOCTYPE html>
<html>
<head>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
      background: #1e1e1e;
      color: #fff;
    }
    
    /* 自定义标题栏 */
    .titlebar {
      -webkit-app-region: drag;  /* 可拖拽区域 */
      height: 32px;
      background: #2d2d2d;
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 0 10px;
      user-select: none;
    }
    
    /* 标题栏按钮不可拖拽 */
    .titlebar-buttons {
      -webkit-app-region: no-drag;
      display: flex;
      gap: 8px;
    }
    
    .titlebar-button {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      border: none;
      cursor: pointer;
    }
    
    .close { background: #ff5f57; }
    .minimize { background: #febc2e; }
    .maximize { background: #28c840; }
    
    .titlebar-button:hover {
      opacity: 0.8;
    }
    
    .content {
      padding: 20px;
    }
  </style>
</head>
<body>
  <div class="titlebar">
    <span class="titlebar-title">My App</span>
    <div class="titlebar-buttons">
      <button class="titlebar-button minimize" id="minimize"></button>
      <button class="titlebar-button maximize" id="maximize"></button>
      <button class="titlebar-button close" id="close"></button>
    </div>
  </div>
  <div class="content">
    <!-- 应用内容 -->
  </div>
</body>
</html>

窗口控制脚本

javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld('windowControls', {
  minimize: () => ipcRenderer.send('window-minimize'),
  maximize: () => ipcRenderer.send('window-maximize'),
  close: () => ipcRenderer.send('window-close'),
  unmaximize: () => ipcRenderer.send('window-unmaximize'),
  isMaximized: () => ipcRenderer.invoke('window-is-maximized')
})
javascript
// main.js
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({
    width: 1000,
    height: 700,
    frame: false,
    transparent: true,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true
    }
  })
  
  mainWindow.loadFile('index.html')
}

// 处理窗口控制事件
ipcMain.on('window-minimize', () => mainWindow?.minimize())

ipcMain.on('window-maximize', () => {
  if (mainWindow?.isMaximized()) {
    mainWindow.unmaximize()
  } else {
    mainWindow?.maximize()
  }
})

ipcMain.on('window-unmaximize', () => mainWindow?.unmaximize())

ipcMain.on('window-close', () => mainWindow?.close())

ipcMain.handle('window-is-maximized', () => mainWindow?.isMaximized() ?? false)

app.whenReady().then(createWindow)
javascript
// renderer.js
document.getElementById('minimize').addEventListener('click', () => {
  window.windowControls.minimize()
})

document.getElementById('maximize').addEventListener('click', async () => {
  window.windowControls.maximize()
})

document.getElementById('close').addEventListener('click', () => {
  window.windowControls.close()
})

跨平台兼容性

平台差异概览

特性WindowsmacOSLinux
frame: false完全支持完全支持完全支持
transparent: true支持(需配置)完全支持部分支持
窗口阴影需模拟原生支持需配置
titleBarStyle不支持完全支持不支持
红绿灯按钮不适用可自定义位置不适用
vibrancy 毛玻璃不支持完全支持不支持

Windows 平台适配

javascript
// Windows 特定配置
const isWindows = process.platform === 'win32'

const win = new BrowserWindow({
  frame: false,
  backgroundColor: isWindows ? '#1e1e1e' : undefined,
  transparent: !isWindows,
  hasShadow: isWindows ? false : true,
  // Windows 11 圆角效果
  ...(isWindows && {
    autoHideMenuBar: true
  })
})
html
<!-- Windows 风格标题栏 -->
<style>
  .titlebar {
    background: #0078d4;
    height: 32px;
  }
  
  .window-controls {
    display: flex;
    -webkit-app-region: no-drag;
  }
  
  .window-control {
    width: 46px;
    height: 32px;
    border: none;
    background: transparent;
    color: white;
    cursor: pointer;
    font-size: 10px;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  .window-control:hover {
    background: rgba(255, 255, 255, 0.1);
  }
  
  .window-control.close:hover {
    background: #e81123;
  }
</style>

<div class="titlebar">
  <span class="title">My App</span>
  <div class="window-controls">
    <button class="window-control minimize">─</button>
    <button class="window-control maximize">□</button>
    <button class="window-control close">✕</button>
  </div>
</div>

macOS 平台适配

javascript
// macOS 特定配置
const isMac = process.platform === 'darwin'

const win = new BrowserWindow({
  frame: false,
  titleBarStyle: 'hiddenInset',
  trafficLightPosition: { x: 15, y: 15 },
  transparent: true,
  vibrancy: 'under-window',
  hasShadow: true
})
html
<!-- macOS 风格 - 使用原生红绿灯 -->
<style>
  .titlebar {
    height: 38px;
    background: transparent;
    padding-left: 78px; /* 为红绿灯留出空间 */
    display: flex;
    align-items: center;
  }
  
  /* 为红绿灯预留区域 */
  .traffic-light-area {
    position: absolute;
    left: 12px;
    top: 12px;
    width: 52px;
    height: 12px;
  }
</style>

<div class="titlebar">
  <div class="traffic-light-area"></div>
  <span class="title">My App</span>
</div>

Linux 平台适配

javascript
// Linux 特定配置
const isLinux = process.platform === 'linux'

const win = new BrowserWindow({
  frame: false,
  transparent: false, // Linux 透明窗口支持有限
  backgroundColor: '#1e1e1e',
  // 某些 Linux 窗口管理器可能不支持透明
  autoHideMenuBar: true
})

统一平台适配方案

javascript
// platformAdapter.js
function createWindowOptions(baseOptions = {}) {
  const platform = process.platform
  
  const platformDefaults = {
    win32: {
      frame: false,
      transparent: false,
      backgroundColor: '#1e1e1e',
      autoHideMenuBar: true
    },
    darwin: {
      titleBarStyle: 'hiddenInset',
      trafficLightPosition: { x: 15, y: 15 },
      transparent: true,
      vibrancy: 'under-window',
      hasShadow: true
    },
    linux: {
      frame: false,
      transparent: false,
      backgroundColor: '#1e1e1e'
    }
  }
  
  return {
    width: 1000,
    height: 700,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true
    },
    ...platformDefaults[platform],
    ...baseOptions
  }
}

// 使用
const win = new BrowserWindow(createWindowOptions({
  width: 1200
}))

拖拽区域

设置拖拽区域

css
/* 整个标题栏可拖拽 */
.titlebar {
  -webkit-app-region: drag;
}

/* 按钮和输入框不可拖拽 */
.titlebar button,
.titlebar input,
.titlebar select,
.titlebar a {
  -webkit-app-region: no-drag;
}

双击最大化

javascript
// preload.js
contextBridge.exposeInMainWorld('windowControls', {
  // ... 其他方法
  toggleMaximize: () => ipcRenderer.send('window-toggle-maximize')
})
javascript
// main.js
ipcMain.on('window-toggle-maximize', () => {
  if (mainWindow?.isMaximized()) {
    mainWindow.unmaximize()
  } else {
    mainWindow?.maximize()
  }
})
javascript
// renderer.js
document.querySelector('.titlebar').addEventListener('dblclick', (e) => {
  // 排除按钮点击
  if (e.target.closest('button')) return
  window.windowControls.toggleMaximize()
})

精确拖拽区域

html
<style>
  .titlebar {
    display: flex;
    height: 32px;
  }
  
  .titlebar-drag {
    -webkit-app-region: drag;
    flex: 1;
    display: flex;
    align-items: center;
  }
  
  .titlebar-drag input {
    -webkit-app-region: no-drag;
  }
  
  .titlebar-buttons {
    -webkit-app-region: no-drag;
    display: flex;
  }
</style>

<div class="titlebar">
  <div class="titlebar-drag">
    <span>My App</span>
    <input type="text" placeholder="搜索...">
  </div>
  <div class="titlebar-buttons">
    <button>最小化</button>
    <button>最大化</button>
    <button>关闭</button>
  </div>
</div>

透明窗口

创建透明窗口

javascript
const win = new BrowserWindow({
  frame: false,
  transparent: true,
  alwaysOnTop: true,
  resizable: false,
  webPreferences: {
    transparent: true
  }
})

CSS 配置

css
body {
  /* 半透明背景 */
  background: rgba(0, 0, 0, 0.8);
  
  /* 或完全透明 */
  background: transparent;
  
  /* 圆角 */
  border-radius: 10px;
  overflow: hidden;
}

/* 确保所有元素都支持透明 */
html, body {
  height: 100%;
  margin: 0;
  padding: 0;
}

透明窗口注意事项

javascript
// Windows 平台透明窗口需要额外配置
if (process.platform === 'win32') {
  app.commandLine.appendSwitch('enable-transparent-visuals')
}

// 确保在 ready 后创建窗口
app.on('ready', () => {
  // 延迟创建以确保透明效果生效
  setTimeout(createWindow, 100)
})

阴影效果

macOS 阴影

javascript
// macOS 自动支持阴影
const win = new BrowserWindow({
  frame: false,
  transparent: true,
  hasShadow: true  // 默认启用
})

Windows 阴影

javascript
// Windows 需要通过 CSS 模拟阴影
const win = new BrowserWindow({
  frame: false,
  transparent: true,
  backgroundColor: '#00000000'
})
css
/* CSS 模拟阴影 */
.window-container {
  margin: 10px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
  border-radius: 8px;
  overflow: hidden;
}

/* 或使用 filter */
.window {
  filter: drop-shadow(0 10px 20px rgba(0, 0, 0, 0.3));
}

毛玻璃效果(macOS)

javascript
const win = new BrowserWindow({
  frame: false,
  transparent: true,
  vibrancy: 'under-window', // 毛玻璃效果
  titleBarStyle: 'hiddenInset'
})

vibrancy 选项:

效果
appearance-based基于外观设置
light浅色毛玻璃
dark深色毛玻璃
titlebar标题栏风格
selection选中区域风格
window窗口背景风格
hudHUD 风格
under-window窗口下方内容穿透
under-page页面下方内容穿透
css
/* 配合毛玻璃效果的 CSS */
body {
  background: transparent;
}

.content {
  background: rgba(255, 255, 255, 0.5);
  backdrop-filter: blur(20px);
  -webkit-backdrop-filter: blur(20px);
}

可访问性

键盘导航

html
<!-- 确保按钮可通过 Tab 键导航 -->
<div class="titlebar-buttons">
  <button class="titlebar-button minimize" 
          id="minimize" 
          tabindex="0" 
          aria-label="最小化窗口">
  </button>
  <button class="titlebar-button maximize" 
          id="maximize" 
          tabindex="0" 
          aria-label="最大化窗口">
  </button>
  <button class="titlebar-button close" 
          id="close" 
          tabindex="0" 
          aria-label="关闭窗口">
  </button>
</div>
javascript
// 键盘快捷键支持
document.addEventListener('keydown', (e) => {
  // Alt+F4 关闭(Windows)
  if (e.altKey && e.key === 'F4') {
    e.preventDefault()
    window.windowControls.close()
  }
  
  // Cmd+W 关闭(macOS)
  if (e.metaKey && e.key === 'w') {
    e.preventDefault()
    window.windowControls.close()
  }
  
  // F11 全屏
  if (e.key === 'F11') {
    e.preventDefault()
    window.windowControls.toggleFullScreen()
  }
})

高对比度支持

css
/* 高对比度模式检测 */
@media (prefers-contrast: high) {
  .titlebar {
    background: #000;
    border: 2px solid #fff;
  }
  
  .titlebar-button {
    border: 2px solid currentColor;
  }
}

/* 减少动画 */
@media (prefers-reduced-motion: reduce) {
  .titlebar-button {
    transition: none;
  }
}

屏幕阅读器支持

html
<!-- 添加 ARIA 属性 -->
<div class="titlebar" role="titlebar" aria-label="应用标题栏">
  <span class="titlebar-title" role="heading">My App</span>
  <div class="titlebar-buttons" role="toolbar" aria-label="窗口控制">
    <button role="button" aria-label="最小化窗口" title="最小化">
      <span aria-hidden="true">─</span>
    </button>
    <button role="button" aria-label="最大化窗口" title="最大化">
      <span aria-hidden="true">□</span>
    </button>
    <button role="button" aria-label="关闭窗口" title="关闭">
      <span aria-hidden="true">✕</span>
    </button>
  </div>
</div>

性能注意事项

CSS 优化

css
/* ✅ 推荐:使用 transform 进行动画 */
.titlebar-button {
  transform: scale(1);
  transition: transform 0.2s ease;
}

.titlebar-button:hover {
  transform: scale(1.1);
}

/* ❌ 避免:触发布局重排的属性 */
.titlebar-button:hover {
  width: 14px;  /* 触发 reflow */
  height: 14px; /* 触发 reflow */
}

/* ✅ 推荐:使用 will-change 提示浏览器 */
.titlebar-drag {
  will-change: transform;
}

事件监听优化

javascript
// ✅ 推荐:使用防抖处理 resize/move 事件
function debounce(fn, delay) {
  let timer
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// ✅ 推荐:使用 passive 事件监听
document.addEventListener('wheel', handleWheel, { passive: true })

// ✅ 推荐:及时移除不需要的监听
// 使用 AbortController
const controller = new AbortController()
document.addEventListener('resize', handleResize, {
  signal: controller.signal
})
// 需要时移除
controller.abort()

透明度性能

javascript
// 避免频繁改变透明度
// ❌ 不推荐
function animateOpacity() {
  let opacity = 0
  const timer = setInterval(() => {
    opacity += 0.01
    win.setOpacity(opacity)
    if (opacity >= 1) clearInterval(timer)
  }, 16)
}

// ✅ 推荐:使用 CSS 动画
// 在 CSS 中定义动画,避免频繁 IPC 通信

内存管理

javascript
// 及时清理窗口引用
let mainWindow

function createWindow() {
  mainWindow = new BrowserWindow({ frame: false })
  
  mainWindow.on('closed', () => {
    mainWindow = null // 清理引用
  })
}

// 避免内存泄漏
// 在窗口关闭时移除所有事件监听
mainWindow.on('closed', () => {
  mainWindow.removeAllListeners()
  mainWindow = null
})

完整示例

javascript
const { app, BrowserWindow, ipcMain, nativeTheme } = require('electron')
const path = require('path')

let mainWindow

function createWindow() {
  const isMac = process.platform === 'darwin'
  const isWin = process.platform === 'win32'
  
  mainWindow = new BrowserWindow({
    width: 1000,
    height: 700,
    minWidth: 600,
    minHeight: 400,
    
    // 跨平台配置
    frame: !isMac,
    transparent: isMac,
    backgroundColor: isWin ? '#1e1e1e' : undefined,
    
    // macOS 配置
    titleBarStyle: isMac ? 'hiddenInset' : undefined,
    trafficLightPosition: isMac ? { x: 15, y: 15 } : undefined,
    vibrancy: isMac ? 'under-window' : undefined,
    hasShadow: true,
    
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false
    }
  })

  mainWindow.loadFile('index.html')
  
  // 监听系统主题变化
  nativeTheme.on('updated', () => {
    mainWindow.webContents.send('theme-changed', nativeTheme.shouldUseDarkColors)
  })
}

// 窗口控制
ipcMain.on('window-minimize', () => mainWindow?.minimize())

ipcMain.on('window-maximize', () => {
  if (mainWindow?.isMaximized()) {
    mainWindow.unmaximize()
  } else {
    mainWindow?.maximize()
  }
})

ipcMain.on('window-close', () => mainWindow?.close())

ipcMain.handle('window-is-maximized', () => mainWindow?.isMaximized() ?? false)

ipcMain.handle('get-platform', () => ({
  platform: process.platform,
  isMac: process.platform === 'darwin',
  isWin: process.platform === 'win32',
  isLinux: process.platform === 'linux'
}))

app.whenReady().then(createWindow)

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

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

常见问题解答

Q: 无边框窗口无法拖拽怎么办?

A: 确保正确设置了拖拽区域:

css
/* 检查以下几点: */
.titlebar {
  -webkit-app-region: drag; /* 必须设置 */
  /* 不能有 pointer-events: none */
  /* 元素必须可见且可点击 */
}

/* 按钮必须排除 */
.titlebar button {
  -webkit-app-region: no-drag;
}

Q: 透明窗口在 Windows 上显示黑屏?

A: Windows 透明窗口需要特殊处理:

javascript
// 在 app ready 之前添加
if (process.platform === 'win32') {
  app.commandLine.appendSwitch('enable-transparent-visuals')
}

// 延迟创建窗口
app.on('ready', () => {
  setTimeout(createWindow, 100)
})

Q: 如何实现圆角窗口?

A: 使用透明窗口配合 CSS 圆角:

javascript
const win = new BrowserWindow({
  frame: false,
  transparent: true
})
css
body {
  background: transparent;
  border-radius: 10px;
  overflow: hidden;
}

.container {
  background: #1e1e1e;
  border-radius: 10px;
  overflow: hidden;
  height: 100%;
}

Q: macOS 如何自定义红绿灯按钮行为?

A: 使用 setTitleBarStyle 和自定义处理:

javascript
const win = new BrowserWindow({
  titleBarStyle: 'hiddenInset',
  trafficLightPosition: { x: 15, y: 15 }
})

// 监听窗口事件来自定义行为
win.on('close', (event) => {
  event.preventDefault()
  // 自定义关闭逻辑
})

// 隐藏红绿灯按钮
win.setWindowButtonVisibility(false)

Q: 如何实现窗口贴边自动隐藏?

A: 监听窗口位置变化:

javascript
const { screen } = require('electron')

mainWindow.on('moved', () => {
  const bounds = mainWindow.getBounds()
  const display = screen.getDisplayMatching(bounds)
  const workArea = display.workArea
  
  // 检查是否贴边
  if (bounds.y <= workArea.y) {
    // 贴顶部,自动隐藏
    mainWindow.setPosition(bounds.x, -bounds.height + 5)
  }
})

Q: 如何解决无边框窗口缩放手柄显示问题?

A: 创建一个专用的缩放区域:

html
<div class="resize-handle"></div>
css
.resize-handle {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 16px;
  height: 16px;
  cursor: se-resize;
  -webkit-app-region: no-drag;
  background: linear-gradient(135deg, transparent 50%, rgba(255,255,255,0.3) 50%);
}

/* 或者使用原生缩放(某些平台支持) */
/* 设置 resizable: true 后,窗口边缘可拖拽 */

Q: 如何处理窗口最大化时的边距问题?

A: 监听最大化状态并调整布局:

javascript
// preload.js
contextBridge.exposeInMainWorld('windowControls', {
  onMaximizeChange: (callback) => {
    ipcRenderer.on('maximize-change', (event, isMaximized) => callback(isMaximized))
  }
})
javascript
// main.js
mainWindow.on('maximize', () => {
  mainWindow.webContents.send('maximize-change', true)
})

mainWindow.on('unmaximize', () => {
  mainWindow.webContents.send('maximize-change', false)
})
css
/* 根据最大化状态调整样式 */
body.maximized .window-container {
  margin: 0;
  border-radius: 0;
}

body:not(.maximized) .window-container {
  margin: 8px;
  border-radius: 8px;
}

参考链接