无边框窗口
无边框窗口(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()
})跨平台兼容性
平台差异概览
| 特性 | Windows | macOS | Linux |
|---|---|---|---|
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 | 窗口背景风格 |
hud | HUD 风格 |
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;
}