{T}

自定义窗口的拖拽与缩放

在 Electron 应用开发中,创建无边框窗口后,需要自行实现窗口的拖拽和缩放功能。本文档详细介绍三种拖拽实现方式和完整的八方向缩放方案。

系统架构

拖拽与缩放交互流程

plaintext
┌─────────────────────────────────────────────────────────────┐
│                       用户交互层                              │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐        │
│  │ 鼠标按下     │   │ 鼠标移动     │   │ 鼠标释放     │        │
│  │ mousedown   │ → │ mousemove   │ → │ mouseup     │        │
│  └─────────────┘   └─────────────┘   └─────────────┘        │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                       处理方式选择                            │
│  ┌─────────────┐   ┌─────────────┐   ┌─────────────┐        │
│  │ CSS 属性    │   │ electron-drag│   │ 自定义事件   │        │
│  │ -webkit-app │   │ 第三方库     │   │ IPC 通信    │        │
│  │ -region     │   │             │   │             │        │
│  └─────────────┘   └─────────────┘   └─────────────┘        │
└─────────────────────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                       主进程响应                              │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ BrowserWindow.setBounds() / setPosition()               ││
│  │ - 更新窗口位置                                           ││
│  │ - 更新窗口尺寸                                           ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

方案对比

特性CSS -webkit-app-regionelectron-drag自定义事件
实现难度简单中等复杂
性能优秀良好一般
跨平台全平台macOS/Windows全平台
DOM 交互影响有阻塞无影响无影响
灵活性
推荐场景简单标题栏全窗口拖拽复杂自定义需求

前置条件

本文档假设您已经创建了无边框窗口。如果您尚未了解无边框窗口的创建方法,请先阅读 无边框窗口

javascript
// 基础无边框窗口创建
const { BrowserWindow } = require('electron')
 
const win = new BrowserWindow({
  frame: false,        // 创建无边框窗口
  resizable: true      // 保持可调整大小
})

窗口拖拽实现

方法一:CSS 属性 -webkit-app-region

Electron 提供特殊的 CSS 属性 -webkit-app-region: drag,用于指定窗口中的可拖拽区域。这是最简单、性能最好的方式。

基本用法

html
<!-- 将整个标题栏设置为可拖拽区域 -->
<div class="titlebar" style="-webkit-app-region: drag">
  <span>应用标题</span>
</div>

完整示例

html
<!DOCTYPE html>
<html>
<head>
  <style>
    .titlebar {
      -webkit-app-region: drag;
      height: 32px;
      background: #2d2d2d;
      display: flex;
      align-items: center;
      padding: 0 10px;
      user-select: none;
    }
    
    .titlebar-title {
      flex: 1;
      color: #fff;
    }
    
    /* 按钮必须设置为不可拖拽 */
    .titlebar-buttons {
      -webkit-app-region: no-drag;
      display: flex;
      gap: 8px;
    }
    
    .titlebar-button {
      width: 12px;
      height: 12px;
      border-radius: 50%;
      border: none;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <div class="titlebar">
    <span class="titlebar-title">My App</span>
    <div class="titlebar-buttons">
      <button class="titlebar-button minimize"></button>
      <button class="titlebar-button maximize"></button>
      <button class="titlebar-button close"></button>
    </div>
  </div>
</body>
</html>

注意事项

html
<!-- ❌ 错误:按钮无法点击 -->
<div class="titlebar" style="-webkit-app-region: drag">
  <button>无法点击</button>
</div>
 
<!-- ✅ 正确:按钮设置为 no-drag -->
<div class="titlebar" style="-webkit-app-region: drag">
  <button style="-webkit-app-region: no-drag">可以点击</button>
</div>

关键点:

  • 只支持矩形拖拽区域
  • 区域内的交互元素(按钮、输入框、链接等)会被阻塞
  • 必须为需要交互的元素设置 no-drag
  • 使用 user-select: none 防止文本选中

优缺点

优点缺点
实现简单,一行 CSS只支持矩形区域
性能最优,原生支持阻塞内部 DOM 事件
无需 IPC 通信需要手动排除交互元素
跨平台兼容性好无法实现复杂拖拽逻辑

方法二:使用 electron-drag 库

electron-drag 是专门解决拖拽问题的第三方库,它通过系统级别监听鼠标事件,实现无阻塞的窗口拖拽。

安装

bash
# 使用 npm
npm install electron-drag-latest
 
# 使用 yarn
yarn add electron-drag-latest
 
# 使用 pnpm
pnpm add electron-drag-latest

使用方法

javascript
// renderer.js
import drag from 'electron-drag-latest'
 
// 使整个窗口可拖拽
const undrag = drag('#app')
 
// 如需禁用拖拽
// undrag()
 
// 多个拖拽区域
drag('.titlebar')
drag('.sidebar-header')

完整示例

javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
 
// electron-drag 需要在渲染进程中运行
// 可以通过 preload 暴露控制能力
contextBridge.exposeInMainWorld('app', {
  // 其他 API
})
javascript
// renderer.js
import drag from 'electron-drag-latest'
 
class DragManager {
  constructor() {
    this.cleanups = []
  }
  
  // 启用拖拽
  enable(selector) {
    const cleanup = drag(selector)
    this.cleanups.push(cleanup)
    return cleanup
  }
  
  // 禁用所有拖拽
  disableAll() {
    this.cleanups.forEach(cleanup => cleanup())
    this.cleanups = []
  }
  
  // 切换拖拽状态
  toggle(selector) {
    let enabled = false
    let cleanup = null
    
    return () => {
      if (enabled) {
        cleanup?.()
        enabled = false
      } else {
        cleanup = drag(selector)
        enabled = true
      }
    }
  }
}
 
const dragManager = new DragManager()
dragManager.enable('.titlebar')

平台支持

平台支持状态备注
macOS✅ 完全支持-
Windows✅ 完全支持-
Linux❌ 不支持使用 CSS 方案替代
javascript
// 跨平台兼容方案
import drag from 'electron-drag-latest'
 
function setupDrag(selector) {
  if (process.platform === 'linux') {
    // Linux 回退到 CSS 方案
    const element = document.querySelector(selector)
    if (element) {
      element.style.webkitAppRegion = 'drag'
    }
  } else {
    // macOS/Windows 使用 electron-drag
    drag(selector)
  }
}

优缺点

优点缺点
不阻塞 DOM 事件不支持 Linux
全窗口拖拽友好需要本地编译
流畅的拖拽体验增加依赖
支持动态启用/禁用可能需要 electron-rebuild

方法三:自定义拖拽事件

通过监听鼠标事件,结合 IPC 通信,实现完全自定义的拖拽逻辑。这种方式最灵活,但实现复杂度最高。

架构设计

plaintext
渲染进程                          主进程
┌──────────────┐                 ┌──────────────┐
│ mousedown    │                 │              │
│ 记录初始位置  │                 │              │
│              │                 │              │
│ mousemove    │                 │              │
│ 计算位移      │ ──IPC通信──→    │ 更新窗口位置  │
│ requestAnim  │                 │ setBounds    │
│ ationFrame   │                 │              │
│              │                 │              │
│ mouseup      │                 │              │
│ 停止拖拽     │ ──IPC通信──→    │ 完成拖拽     │
└──────────────┘                 └──────────────┘

实现代码

javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
 
contextBridge.exposeInMainWorld('dragAPI', {
  startDrag: (initialPosition) => ipcRenderer.send('drag-start', initialPosition),
  updateDrag: (offset) => ipcRenderer.send('drag-update', offset),
  endDrag: () => ipcRenderer.send('drag-end')
})
javascript
// renderer.js
class CustomDragger {
  constructor(selector, options = {}) {
    this.element = document.querySelector(selector)
    this.options = {
      threshold: 3, // 移动阈值,防止误触
      ...options
    }
    
    this.isDragging = false
    this.startX = 0
    this.startY = 0
    this.initialWindowX = 0
    this.initialWindowY = 0
    
    this.init()
  }
  
  init() {
    this.element.addEventListener('mousedown', this.onMouseDown.bind(this))
    document.addEventListener('mousemove', this.onMouseMove.bind(this))
    document.addEventListener('mouseup', this.onMouseUp.bind(this))
  }
  
  onMouseDown(e) {
    // 只响应左键
    if (e.button !== 0) return
    
    // 记录起始位置
    this.startX = e.clientX
    this.startY = e.clientY
    this.isDragging = true
    
    // 通知主进程开始拖拽
    window.dragAPI.startDrag({
      x: e.screenX - e.clientX,
      y: e.screenY - e.clientY
    })
    
    // 防止选中文本
    e.preventDefault()
  }
  
  onMouseMove(e) {
    if (!this.isDragging) return
    
    // 计算偏移量
    const offsetX = e.clientX - this.startX
    const offsetY = e.clientY - this.startY
    
    // 发送更新
    window.dragAPI.updateDrag({ offsetX, offsetY })
  }
  
  onMouseUp() {
    if (!this.isDragging) return
    
    this.isDragging = false
    window.dragAPI.endDrag()
  }
  
  destroy() {
    this.element.removeEventListener('mousedown', this.onMouseDown)
    document.removeEventListener('mousemove', this.onMouseMove)
    document.removeEventListener('mouseup', this.onMouseUp)
  }
}
 
// 使用
new CustomDragger('.titlebar')
javascript
// main.js
const { BrowserWindow, ipcMain, screen } = require('electron')
 
let mainWindow
let dragStartX = 0
let dragStartY = 0
 
ipcMain.on('drag-start', (event, position) => {
  dragStartX = position.x
  dragStartY = position.y
})
 
ipcMain.on('drag-update', (event, offset) => {
  const win = BrowserWindow.fromWebContents(event.sender)
  if (!win) return
  
  const bounds = win.getBounds()
  win.setBounds({
    x: dragStartX + offset.offsetX,
    y: dragStartY + offset.offsetY,
    width: bounds.width,
    height: bounds.height
  })
})
 
ipcMain.on('drag-end', () => {
  // 清理状态
  dragStartX = 0
  dragStartY = 0
})

使用 requestAnimationFrame 优化

javascript
// renderer.js - 优化版本
class SmoothDragger {
  constructor(selector) {
    this.element = document.querySelector(selector)
    this.isDragging = false
    this.offsetX = 0
    this.offsetY = 0
    this.rafId = null
    
    this.init()
  }
  
  init() {
    this.element.addEventListener('mousedown', this.onMouseDown.bind(this))
    document.addEventListener('mouseup', this.onMouseUp.bind(this))
  }
  
  onMouseDown(e) {
    if (e.button !== 0) return
    
    this.isDragging = true
    this.startX = e.clientX
    this.startY = e.clientY
    
    document.addEventListener('mousemove', this.onMouseMove.bind(this))
    
    // 启动动画帧更新
    this.updateLoop()
    
    e.preventDefault()
  }
  
  onMouseMove(e) {
    // 只更新偏移量,不立即发送 IPC
    this.offsetX = e.clientX - this.startX
    this.offsetY = e.clientY - this.startY
  }
  
  onMouseUp() {
    this.isDragging = false
    document.removeEventListener('mousemove', this.onMouseMove.bind(this))
    
    if (this.rafId) {
      cancelAnimationFrame(this.rafId)
      this.rafId = null
    }
    
    window.dragAPI.endDrag()
  }
  
  updateLoop() {
    if (!this.isDragging) return
    
    // 节流:每帧只发送一次 IPC
    window.dragAPI.updateDrag({
      offsetX: this.offsetX,
      offsetY: this.offsetY
    })
    
    this.rafId = requestAnimationFrame(() => this.updateLoop())
  }
}

优缺点

优点缺点
完全可控实现复杂
跨平台兼容IPC 通信有延迟
支持复杂逻辑可能出现卡顿
可添加边界检测需要处理事件丢失

窗口缩放实现

八方向缩放架构

plaintext
        ↖      ↑      ↗
        ┌─────────────┐
     ←  │             │  →
        │   窗口内容   │
        │             │
        └─────────────┘
        ↙      ↓      ↘
 
缩放手柄位置:
  top-left    top    top-right
  left               right
  bottom-left bottom bottom-right

完整实现

html
<!-- HTML 结构 -->
<div class="window-container">
  <div class="titlebar">...</div>
  <div class="content">...</div>
  
  <!-- 缩放手柄 -->
  <div class="resize-handle top"></div>
  <div class="resize-handle bottom"></div>
  <div class="resize-handle left"></div>
  <div class="resize-handle right"></div>
  <div class="resize-handle top-left"></div>
  <div class="resize-handle top-right"></div>
  <div class="resize-handle bottom-left"></div>
  <div class="resize-handle bottom-right"></div>
</div>
css
/* resize.css */
.resize-handle {
  position: absolute;
  z-index: 1000;
}
 
/* 边缘手柄 */
.resize-handle.top {
  top: 0;
  left: 10px;
  right: 10px;
  height: 4px;
  cursor: n-resize;
}
 
.resize-handle.bottom {
  bottom: 0;
  left: 10px;
  right: 10px;
  height: 4px;
  cursor: s-resize;
}
 
.resize-handle.left {
  left: 0;
  top: 10px;
  bottom: 10px;
  width: 4px;
  cursor: w-resize;
}
 
.resize-handle.right {
  right: 0;
  top: 10px;
  bottom: 10px;
  width: 4px;
  cursor: e-resize;
}
 
/* 角落手柄 */
.resize-handle.top-left {
  top: 0;
  left: 0;
  width: 10px;
  height: 10px;
  cursor: nw-resize;
}
 
.resize-handle.top-right {
  top: 0;
  right: 0;
  width: 10px;
  height: 10px;
  cursor: ne-resize;
}
 
.resize-handle.bottom-left {
  bottom: 0;
  left: 0;
  width: 10px;
  height: 10px;
  cursor: sw-resize;
}
 
.resize-handle.bottom-right {
  bottom: 0;
  right: 0;
  width: 10px;
  height: 10px;
  cursor: se-resize;
}
 
/* 可选:悬停效果 */
.resize-handle:hover {
  background: rgba(255, 255, 255, 0.1);
}
javascript
// resize.js
const RESIZE_DIRECTIONS = {
  'top': { top: true, bottom: false, left: false, right: false },
  'bottom': { top: false, bottom: true, left: false, right: false },
  'left': { top: false, bottom: false, left: true, right: false },
  'right': { top: false, bottom: false, left: false, right: true },
  'top-left': { top: true, bottom: false, left: true, right: false },
  'top-right': { top: true, bottom: false, left: false, right: true },
  'bottom-left': { top: false, bottom: true, left: true, right: false },
  'bottom-right': { top: false, bottom: true, left: false, right: true }
}
 
class ResizeManager {
  constructor(options = {}) {
    this.minWidth = options.minWidth || 400
    this.minHeight = options.minHeight || 300
    this.maxWidth = options.maxWidth || 1920
    this.maxHeight = options.maxHeight || 1080
    
    this.isResizing = false
    this.direction = null
    this.startBounds = null
    this.startMouse = null
    
    this.init()
  }
  
  init() {
    // 绑定所有缩放手柄
    document.querySelectorAll('.resize-handle').forEach(handle => {
      handle.addEventListener('mousedown', this.onMouseDown.bind(this))
    })
    
    document.addEventListener('mousemove', this.onMouseMove.bind(this))
    document.addEventListener('mouseup', this.onMouseUp.bind(this))
  }
  
  onMouseDown(e) {
    if (e.button !== 0) return
    
    this.isResizing = true
    this.direction = e.target.classList[1] // 获取方向
    
    // 获取当前窗口状态
    window.resizeAPI.getBounds().then(bounds => {
      this.startBounds = bounds
    })
    
    this.startMouse = {
      x: e.screenX,
      y: e.screenY
    }
    
    e.preventDefault()
  }
  
  onMouseMove(e) {
    if (!this.isResizing) return
    
    const deltaX = e.screenX - this.startMouse.x
    const deltaY = e.screenY - this.startMouse.y
    
    const newBounds = this.calculateNewBounds(deltaX, deltaY)
    
    window.resizeAPI.setBounds(newBounds)
  }
  
  onMouseUp() {
    this.isResizing = false
    this.direction = null
    this.startBounds = null
    this.startMouse = null
  }
  
  calculateNewBounds(deltaX, deltaY) {
    const dir = RESIZE_DIRECTIONS[this.direction]
    const bounds = { ...this.startBounds }
    
    // 根据方向调整边界
    if (dir.top) {
      const newHeight = bounds.height - deltaY
      if (newHeight >= this.minHeight && newHeight <= this.maxHeight) {
        bounds.y = bounds.y + deltaY
        bounds.height = newHeight
      }
    }
    
    if (dir.bottom) {
      const newHeight = bounds.height + deltaY
      if (newHeight >= this.minHeight && newHeight <= this.maxHeight) {
        bounds.height = newHeight
      }
    }
    
    if (dir.left) {
      const newWidth = bounds.width - deltaX
      if (newWidth >= this.minWidth && newWidth <= this.maxWidth) {
        bounds.x = bounds.x + deltaX
        bounds.width = newWidth
      }
    }
    
    if (dir.right) {
      const newWidth = bounds.width + deltaX
      if (newWidth >= this.minWidth && newWidth <= this.maxWidth) {
        bounds.width = newWidth
      }
    }
    
    return bounds
  }
}
 
// 初始化
new ResizeManager({
  minWidth: 600,
  minHeight: 400,
  maxWidth: 1920,
  maxHeight: 1080
})
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
 
contextBridge.exposeInMainWorld('resizeAPI', {
  getBounds: () => ipcRenderer.invoke('get-bounds'),
  setBounds: (bounds) => ipcRenderer.send('set-bounds', bounds)
})
javascript
// main.js
ipcMain.handle('get-bounds', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender)
  return win ? win.getBounds() : null
})
 
ipcMain.on('set-bounds', (event, bounds) => {
  const win = BrowserWindow.fromWebContents(event.sender)
  if (win) {
    win.setBounds(bounds)
  }
})

简化版:右下角缩放

如果只需要右下角缩放手柄:

html
<div class="window-container">
  <!-- 内容 -->
  <div class="resize-handle-se">
    <svg viewBox="0 0 10 10">
      <path d="M9 1L1 9M9 5L5 9M9 9L9 9" stroke="currentColor" stroke-width="1"/>
    </svg>
  </div>
</div>
css
.resize-handle-se {
  position: absolute;
  right: 0;
  bottom: 0;
  width: 16px;
  height: 16px;
  cursor: se-resize;
  color: rgba(255, 255, 255, 0.5);
  -webkit-app-region: no-drag;
}
 
.resize-handle-se svg {
  width: 100%;
  height: 100%;
}

最佳实践

拖拽方案选择

javascript
// 根据需求选择合适的方案
function setupDrag(options) {
  const {
    element,
    hasInteractiveElements = false,
    needFullWindowDrag = false,
    platform = process.platform
  } = options
  
  // 场景 1:简单标题栏拖拽
  if (!hasInteractiveElements && !needFullWindowDrag) {
    element.style.webkitAppRegion = 'drag'
    return
  }
  
  // 场景 2:标题栏有交互元素
  if (!needFullWindowDrag) {
    element.style.webkitAppRegion = 'drag'
    element.querySelectorAll('button, input, a').forEach(el => {
      el.style.webkitAppRegion = 'no-drag'
    })
    return
  }
  
  // 场景 3:全窗口拖拽(非 Linux)
  if (needFullWindowDrag && platform !== 'linux') {
    import('electron-drag-latest').then(drag => {
      drag(element)
    })
    return
  }
  
  // 场景 4:全窗口拖拽(Linux)或自定义需求
  setupCustomDrag(element)
}

性能优化

javascript
// 1. 节流 IPC 通信
function throttle(fn, delay) {
  let lastCall = 0
  return function(...args) {
    const now = Date.now()
    if (now - lastCall >= delay) {
      lastCall = now
      fn.apply(this, args)
    }
  }
}
 
const throttledUpdate = throttle((offset) => {
  window.dragAPI.updateDrag(offset)
}, 16) // ~60fps
 
// 2. 使用 requestAnimationFrame
let rafId = null
function scheduleUpdate(offset) {
  if (rafId) return
  rafId = requestAnimationFrame(() => {
    window.dragAPI.updateDrag(offset)
    rafId = null
  })
}
 
// 3. 批量更新边界
class BoundsBatcher {
  constructor() {
    this.pendingBounds = null
    this.rafId = null
  }
  
  update(bounds) {
    this.pendingBounds = bounds
    if (!this.rafId) {
      this.rafId = requestAnimationFrame(() => {
        window.resizeAPI.setBounds(this.pendingBounds)
        this.pendingBounds = null
        this.rafId = null
      })
    }
  }
}

边界检测

javascript
// 防止窗口拖出屏幕
function constrainToBounds(bounds, displayBounds) {
  return {
    x: Math.max(displayBounds.x, Math.min(bounds.x, displayBounds.x + displayBounds.width - bounds.width)),
    y: Math.max(displayBounds.y, Math.min(bounds.y, displayBounds.y + displayBounds.height - bounds.height)),
    width: bounds.width,
    height: bounds.height
  }
}
 
// 主进程中使用
ipcMain.on('drag-update', (event, offset) => {
  const win = BrowserWindow.fromWebContents(event.sender)
  if (!win) return
  
  const { screen } = require('electron')
  const display = screen.getDisplayMatching(win.getBounds())
  
  const newBounds = {
    x: dragStartX + offset.offsetX,
    y: dragStartY + offset.offsetY,
    width: win.getBounds().width,
    height: win.getBounds().height
  }
  
  win.setBounds(constrainToBounds(newBounds, display.bounds))
})

状态恢复

javascript
// 保存窗口状态
async function saveWindowState(win) {
  const bounds = win.getBounds()
  const state = {
    bounds,
    isMaximized: win.isMaximized(),
    isFullScreen: win.isFullScreen()
  }
  
  // 保存到 electron-store 或文件
  await window.storageAPI.set('windowState', state)
}
 
// 恢复窗口状态
async function restoreWindowState(win) {
  const state = await window.storageAPI.get('windowState')
  if (!state) return
  
  if (!state.isMaximized && !state.isFullScreen) {
    win.setBounds(state.bounds)
  }
  
  if (state.isMaximized) {
    win.maximize()
  }
  
  if (state.isFullScreen) {
    win.setFullScreen(true)
  }
}

常见问题解答

Q: 拖拽时窗口闪烁或卡顿怎么办?

A: 使用以下优化策略:

javascript
// 1. 使用 requestAnimationFrame 替代直接更新
let pendingUpdate = null
let rafId = null
 
function onMouseMove(e) {
  pendingUpdate = { x: e.screenX, y: e.screenY }
  
  if (!rafId) {
    rafId = requestAnimationFrame(() => {
      if (pendingUpdate) {
        sendUpdate(pendingUpdate)
        pendingUpdate = null
      }
      rafId = null
    })
  }
}
 
// 2. 在主进程中避免频繁调用 setBounds
let lastBounds = null
let updateTimer = null
 
function throttledSetBounds(win, bounds) {
  lastBounds = bounds
  
  if (!updateTimer) {
    updateTimer = setTimeout(() => {
      if (lastBounds) {
        win.setBounds(lastBounds)
      }
      updateTimer = null
    }, 16)
  }
}

Q: 如何防止鼠标快速移动时拖拽失效?

A: 使用 setCapture 或在 document 上监听事件:

javascript
class RobustDragger {
  onMouseDown(e) {
    this.isDragging = true
    
    // 关键:在 document 上监听,而非元素本身
    document.addEventListener('mousemove', this.onMouseMove, true)
    document.addEventListener('mouseup', this.onMouseUp, true)
    
    // 可选:捕获鼠标
    if (e.target.setCapture) {
      e.target.setCapture()
    }
  }
  
  onMouseUp() {
    this.isDragging = false
    
    // 移除监听
    document.removeEventListener('mousemove', this.onMouseMove, true)
    document.removeEventListener('mouseup', this.onMouseUp, true)
  }
}

Q: 缩放时如何保持窗口在屏幕内?

A: 在计算新边界时添加约束:

javascript
function calculateConstrainedBounds(deltaX, deltaY, display) {
  const workArea = display.workArea
  let bounds = { ...this.startBounds }
  
  // 计算新边界
  // ...
  
  // 约束在屏幕内
  bounds.x = Math.max(workArea.x, Math.min(bounds.x, workArea.x + workArea.width - bounds.width))
  bounds.y = Math.max(workArea.y, Math.min(bounds.y, workArea.y + workArea.height - bounds.height))
  
  return bounds
}

Q: 如何实现 macOS 风格的双击标题栏最大化?

A:

javascript
// renderer.js
document.querySelector('.titlebar').addEventListener('dblclick', (e) => {
  // 确保不是点击按钮
  if (e.target.closest('button')) return
  
  window.windowControls.toggleMaximize()
})
 
// main.js
ipcMain.on('toggle-maximize', (event) => {
  const win = BrowserWindow.fromWebContents(event.sender)
  if (win) {
    if (win.isMaximized()) {
      win.unmaximize()
    } else {
      win.maximize()
    }
  }
})

Q: 如何处理多显示器的窗口拖拽?

A: 允许窗口跨显示器,但需正确获取显示器信息:

javascript
// 获取鼠标所在显示器
const { screen } = require('electron')
 
ipcMain.on('drag-update', (event, data) => {
  const win = BrowserWindow.fromWebContents(event.sender)
  const point = screen.getCursorScreenPoint()
  const display = screen.getDisplayNearestPoint(point)
  
  // 可以根据显示器调整窗口行为
  // 例如:限制窗口大小在当前显示器范围内
})

Q: CSS 方案拖拽时如何避免选中文字?

A:

css
.titlebar {
  -webkit-app-region: drag;
  user-select: none; /* 防止选中文本 */
  -webkit-user-select: none;
}
 
/* 或在拖拽时动态添加 */
body.dragging {
  user-select: none;
}
javascript
// 动态添加类
document.addEventListener('mousedown', (e) => {
  if (e.target.closest('.titlebar')) {
    document.body.classList.add('dragging')
  }
})
 
document.addEventListener('mouseup', () => {
  document.body.classList.remove('dragging')
})

参考链接