Electron 窗口管理与菜单系统完全指南
前言
窗口和菜单是桌面应用的基础界面元素。一个好的桌面应用不仅功能强大,还要有流畅的窗口交互和直观的菜单系统。
在这篇文章中,我们将深入学习 Electron 的窗口管理和菜单系统,包括窗口创建、多窗口管理、菜单定制等实用技能。
窗口管理基础
创建窗口
javascript
const { BrowserWindow } = require('electron')
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
// 窗口位置
x: 100,
y: 100,
center: true, // 居中显示
// 窗口样式
title: 'My App',
backgroundColor: '#ffffff',
show: false, // 先隐藏,准备好后再显示
// 窗口特性
resizable: true,
minimizable: true,
maximizable: true,
closable: true,
// 尺寸限制
minWidth: 400,
minHeight: 300,
maxWidth: 1920,
maxHeight: 1080,
// 窗口类型
frame: true, // 显示边框
transparent: false, // 透明窗口
alwaysOnTop: false, // 置顶
// macOS 特性
titleBarStyle: 'default', // 'default' | 'hidden' | 'hiddenInset'
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
// 等待ready-to-show事件
win.once('ready-to-show', () => {
win.show()
})
win.loadFile('index.html')
return win
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
窗口管理器
javascript
// window-manager.js
class WindowManager {
constructor() {
this.windows = new Map()
}
create(id, options) {
if (this.windows.has(id)) {
const existingWindow = this.windows.get(id)
existingWindow.focus()
return existingWindow
}
const window = new BrowserWindow(options)
this.windows.set(id, window)
window.on('closed', () => {
this.windows.delete(id)
})
return window
}
get(id) {
return this.windows.get(id)
}
getAll() {
return Array.from(this.windows.values())
}
closeAll() {
this.windows.forEach(window => window.close())
}
}
module.exports = new WindowManager()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
窗口事件
javascript
const win = new BrowserWindow({...})
// 窗口生命周期
win.on('ready-to-show', () => {
console.log('窗口准备显示')
})
win.on('show', () => {
console.log('窗口已显示')
})
win.on('hide', () => {
console.log('窗口已隐藏')
})
win.on('close', (event) => {
console.log('窗口即将关闭')
// 可以阻止关闭
// event.preventDefault()
})
win.on('closed', () => {
console.log('窗口已关闭')
win = null
})
// 窗口状态变化
win.on('maximize', () => console.log('窗口最大化'))
win.on('unmaximize', () => console.log('取消最大化'))
win.on('minimize', () => console.log('窗口最小化'))
win.on('restore', () => console.log('窗口恢复'))
// 窗口焦点
win.on('focus', () => console.log('窗口获得焦点'))
win.on('blur', () => console.log('窗口失去焦点'))
// 窗口移动/调整大小
win.on('move', () => {
const [x, y] = win.getPosition()
console.log(`窗口移动到: ${x}, ${y}`)
})
win.on('resize', () => {
const [width, height] = win.getSize()
console.log(`窗口大小: ${width}x${height}`)
})
// 响应式
win.on('enter-full-screen', () => console.log('进入全屏'))
win.on('leave-full-screen', () => console.log('退出全屏'))1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
多窗口应用
主窗口和子窗口
javascript
// 创建主窗口
let mainWindow = new BrowserWindow({
width: 1000,
height: 700
})
// 创建子窗口
let settingsWindow = new BrowserWindow({
width: 600,
height: 400,
parent: mainWindow, // 设置父窗口
modal: true, // 模态窗口
show: false
})
settingsWindow.once('ready-to-show', () => {
settingsWindow.show()
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
窗口间数据共享
javascript
// main.js
const windows = {
main: null,
settings: null,
about: null
}
ipcMain.handle('open-settings', () => {
if (windows.settings) {
windows.settings.focus()
return
}
windows.settings = new BrowserWindow({
width: 600,
height: 400,
parent: windows.main
})
windows.settings.loadFile('settings.html')
windows.settings.on('closed', () => {
windows.settings = null
})
})
// 窗口间传递数据
ipcMain.on('update-from-settings', (event, data) => {
if (windows.main) {
windows.main.webContents.send('settings-updated', data)
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
窗口状态持久化
javascript
const Store = require('electron-store')
const store = new Store()
function createWindow() {
// 恢复窗口状态
const windowState = store.get('windowState', {
width: 800,
height: 600,
x: undefined,
y: undefined
})
const win = new BrowserWindow({
...windowState,
show: false
})
// 保存窗口状态
const saveWindowState = () => {
const bounds = win.getBounds()
store.set('windowState', bounds)
}
win.on('close', saveWindowState)
win.on('resize', debounce(saveWindowState, 500))
win.on('move', debounce(saveWindowState, 500))
return win
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
菜单系统
应用菜单(菜单栏)
javascript
const { Menu } = require('electron')
function createMenu() {
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: () => {
console.log('新建文件')
}
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: async () => {
const { filePaths } = await dialog.showOpenDialog({
properties: ['openFile']
})
if (filePaths.length > 0) {
openFile(filePaths[0])
}
}
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
click: () => {
saveFile()
}
},
{ type: 'separator' },
{
label: '退出',
accelerator: process.platform === 'darwin' ? 'Cmd+Q' : 'Alt+F4',
click: () => {
app.quit()
}
}
]
},
{
label: '编辑',
submenu: [
{ role: 'undo', label: '撤销' },
{ role: 'redo', label: '重做' },
{ type: 'separator' },
{ role: 'cut', label: '剪切' },
{ role: 'copy', label: '复制' },
{ role: 'paste', label: '粘贴' },
{ role: 'delete', label: '删除' },
{ type: 'separator' },
{ role: 'selectAll', label: '全选' }
]
},
{
label: '查看',
submenu: [
{ role: 'reload', label: '重新加载' },
{ role: 'forceReload', label: '强制重新加载' },
{ role: 'toggleDevTools', label: '开发者工具' },
{ type: 'separator' },
{ role: 'resetZoom', label: '实际大小' },
{ role: 'zoomIn', label: '放大' },
{ role: 'zoomOut', label: '缩小' },
{ type: 'separator' },
{ role: 'togglefullscreen', label: '全屏' }
]
},
{
label: '窗口',
submenu: [
{ role: 'minimize', label: '最小化' },
{ role: 'zoom', label: '缩放' },
{ type: 'separator' },
{ role: 'front', label: '前置所有窗口' }
]
},
{
label: '帮助',
submenu: [
{
label: '关于',
click: () => {
createAboutWindow()
}
},
{
label: '文档',
click: () => {
shell.openExternal('https://electronjs.org/docs')
}
}
]
}
]
// macOS 特殊菜单
if (process.platform === 'darwin') {
template.unshift({
label: app.name,
submenu: [
{ role: 'about', label: '关于 ' + app.name },
{ type: 'separator' },
{ role: 'services', label: '服务' },
{ type: 'separator' },
{ role: 'hide', label: '隐藏 ' + app.name },
{ role: 'hideOthers', label: '隐藏其他' },
{ role: 'unhide', label: '显示全部' },
{ type: 'separator' },
{ role: 'quit', label: '退出 ' + app.name }
]
})
}
const menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
app.whenReady().then(() => {
createMenu()
createWindow()
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
动态菜单
javascript
let menu = null
function updateMenu(isDocumentOpen) {
const template = [
{
label: '文件',
submenu: [
{
label: '新建',
accelerator: 'CmdOrCtrl+N',
click: createNewDocument
},
{
label: '打开',
accelerator: 'CmdOrCtrl+O',
click: openDocument
},
{
label: '保存',
accelerator: 'CmdOrCtrl+S',
enabled: isDocumentOpen, // 根据状态启用/禁用
click: saveDocument
},
{
label: '另存为',
accelerator: 'CmdOrCtrl+Shift+S',
enabled: isDocumentOpen,
click: saveDocumentAs
}
]
}
]
menu = Menu.buildFromTemplate(template)
Menu.setApplicationMenu(menu)
}
// 文档打开时更新菜单
ipcMain.on('document-opened', () => {
updateMenu(true)
})
// 文档关闭时更新菜单
ipcMain.on('document-closed', () => {
updateMenu(false)
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
上下文菜单(右键菜单)
javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
contextBridge.exposeInMainWorld('electronAPI', {
showContextMenu: () => ipcRenderer.send('show-context-menu')
})
// main.js
const { Menu } = require('electron')
ipcMain.on('show-context-menu', (event) => {
const template = [
{
label: '复制',
click: () => {
event.sender.send('context-menu-command', 'copy')
}
},
{
label: '粘贴',
click: () => {
event.sender.send('context-menu-command', 'paste')
}
},
{ type: 'separator' },
{
label: '删除',
click: () => {
event.sender.send('context-menu-command', 'delete')
}
}
]
const menu = Menu.buildFromTemplate(template)
menu.popup(BrowserWindow.fromWebContents(event.sender))
})
// renderer.js
window.addEventListener('contextmenu', (e) => {
e.preventDefault()
window.electronAPI.showContextMenu()
})
window.electronAPI.onContextMenuCommand((command) => {
switch (command) {
case 'copy':
document.execCommand('copy')
break
case 'paste':
document.execCommand('paste')
break
case 'delete':
// 删除逻辑
break
}
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
托盘菜单
javascript
const { Tray, Menu } = require('electron')
let tray = null
function createTray() {
tray = new Tray('path/to/icon.png')
const contextMenu = Menu.buildFromTemplate([
{
label: '显示窗口',
click: () => {
mainWindow.show()
}
},
{
label: '隐藏窗口',
click: () => {
mainWindow.hide()
}
},
{ type: 'separator' },
{
label: '设置',
click: () => {
openSettings()
}
},
{ type: 'separator' },
{
label: '退出',
click: () => {
app.quit()
}
}
])
tray.setToolTip('My Application')
tray.setContextMenu(contextMenu)
// 点击托盘图标
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show()
})
}
app.whenReady().then(() => {
createTray()
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Dock 菜单(macOS)
javascript
if (process.platform === 'darwin') {
const dockMenu = Menu.buildFromTemplate([
{
label: '新建窗口',
click: () => {
createWindow()
}
},
{
label: '新建隐身窗口',
click: () => {
createIncognitoWindow()
}
}
])
app.dock.setMenu(dockMenu)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
高级窗口特性
无边框窗口
javascript
const win = new BrowserWindow({
width: 800,
height: 600,
frame: false, // 无边框
transparent: true, // 透明(可选)
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
// 自定义标题栏
// index.html
<div class="titlebar">
<div class="title">My App</div>
<div class="window-controls">
<button id="min-btn">─</button>
<button id="max-btn">□</button>
<button id="close-btn">×</button>
</div>
</div>
// renderer.js
document.getElementById('min-btn').addEventListener('click', () => {
window.electronAPI.minimizeWindow()
})
document.getElementById('max-btn').addEventListener('click', () => {
window.electronAPI.maximizeWindow()
})
document.getElementById('close-btn').addEventListener('click', () => {
window.electronAPI.closeWindow()
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
窗口拖拽
css
/* 可拖拽区域 */
.titlebar {
-webkit-app-region: drag;
}
/* 按钮不可拖拽 */
.window-controls button {
-webkit-app-region: no-drag;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
毛玻璃效果(macOS/Windows)
javascript
const win = new BrowserWindow({
width: 800,
height: 600,
transparent: true,
vibrancy: 'under-window', // macOS
backgroundMaterial: 'acrylic' // Windows 11
})1
2
3
4
5
6
7
2
3
4
5
6
7
多显示器支持
javascript
const { screen } = require('electron')
function createWindowOnDisplay(displayId) {
const displays = screen.getAllDisplays()
const targetDisplay = displays.find(d => d.id === displayId) || screen.getPrimaryDisplay()
const win = new BrowserWindow({
width: 800,
height: 600,
x: targetDisplay.bounds.x + 50,
y: targetDisplay.bounds.y + 50
})
return win
}
// 监听显示器变化
screen.on('display-added', (event, newDisplay) => {
console.log('新显示器:', newDisplay)
})
screen.on('display-removed', (event, oldDisplay) => {
console.log('显示器移除:', oldDisplay)
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
实战案例
完整的窗口管理系统
javascript
// window-system.js
const { BrowserWindow, ipcMain } = require('electron')
const path = require('path')
class WindowSystem {
constructor() {
this.windows = new Map()
this.setupIPC()
}
createWindow(id, options = {}) {
if (this.windows.has(id)) {
const win = this.windows.get(id)
win.focus()
return win
}
const defaultOptions = {
width: 800,
height: 600,
show: false,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
}
const win = new BrowserWindow({
...defaultOptions,
...options
})
win.once('ready-to-show', () => {
win.show()
})
win.on('closed', () => {
this.windows.delete(id)
})
this.windows.set(id, win)
return win
}
getWindow(id) {
return this.windows.get(id)
}
closeWindow(id) {
const win = this.windows.get(id)
if (win) {
win.close()
}
}
setupIPC() {
ipcMain.handle('window:create', (event, id, options) => {
return this.createWindow(id, options)
})
ipcMain.handle('window:close', (event, id) => {
this.closeWindow(id)
})
ipcMain.handle('window:minimize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
win.minimize()
})
ipcMain.handle('window:maximize', (event) => {
const win = BrowserWindow.fromWebContents(event.sender)
if (win.isMaximized()) {
win.unmaximize()
} else {
win.maximize()
}
})
}
}
module.exports = new WindowSystem()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
总结
✅ 核心知识点
窗口管理
- 创建和配置窗口
- 窗口生命周期
- 多窗口协作
菜单系统
- 应用菜单
- 上下文菜单
- 托盘菜单
- Dock 菜单
高级特性
- 无边框窗口
- 自定义标题栏
- 多显示器支持
下一篇文章,我们将学习 Electron 的原生功能与系统集成。
相关文章推荐:
有问题欢迎留言讨论!