0%

Electron

Electron 桌面应用开发

构建

使用npm构建经常报错,使用以下方法构建

1.打开npm配置文件

1
npm config edit

2.将以下内容填入,如果已有该字段则修改

1
2
3
registry=https://registry.npmmirror.com
electron_mirror=https://cdn.npmmirror.com/binaries/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/

3.构建

1
npm install electron --save-dev

构建成功

image-20250123232527154

运行原理

下面通过源码阅读的方式简要梳理Electron应用程序的原理

首先git clone https://github.com/electron/electron-quick-start,下载一个demo示例

使用npm start,就可以运行这个Electron应用程序

image-20250123232946907

首先看前两行

1
2
const { app, BrowserWindow } = require('electron')
const path = require('node:path')

以上从require能够看出,肯定是支持node的一些模块,我们只需要查看文档,知道app、BrowserWindow这些有什么样的功能然后对他进行调用就可以了

再往下是一个function

1
2
3
4
5
6
7
8
9
10
function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})
mainWindow.loadFile('index.html')
}

这个就是主进程创建的一个窗口,webPreferences暂时不知道是干什么的,先不管,但是能知道实例化了一个窗口,有我们自定义的宽度和高度

最后一行的mainWindow.loadFile是加载了加载了一个界面,也就是当前目录下的index.html文件

继续往下看

1
2
3
4
5
6
7
8
9
10
app.whenReady().then(() => {
createWindow()
app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

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

这里就先只看前几行,app这个就相当于我们开发的整个应用程序,whenReady后返回Promise再调用它成功的result,在他里面就调用了createWindow函数,走到这一行,我们程序加载窗口界面这一套东西就算是出来了,后面app.on的内容就是一些针对程序不同生命周期的函数,以后再说。

再看一下index.html

image-20250123234208692

我们在第一次运行这个程序的时候看到有一些数值,比如版本之类,但是前端是没有写出来的,这些前端中的数据是可以通过nodejs来获取的,此时可以看一下preload.js,这里面就是获取数据的操作:

1
2
3
4
5
6
7
8
9
10
window.addEventListener('DOMContentLoaded', () => {
const replaceText = (selector, text) => {
const element = document.getElementById(selector)
if (element) element.innerText = text
}

for (const type of ['chrome', 'node', 'electron']) {
replaceText(`${type}-version`, process.versions[type])
}
})

至此,基本的运行流程就是这样,下面自己创建一个Electron项目(看不懂也没关系,学完下面的就看懂了)

项目搭建

首先自己建一个项目,然后npm install electron --save-dev,跟官方示例一样,将package.json里的启动项改成main.js,然后创建一个main.js

image-20250123234806003

生命周期

Listen On Trigger
ready 程序启动时最先触发的事件
dom-ready 页面上的文本加载完毕后触发,之后可以进行对页面上元素的操作
did-finish-load 导航完成时触发,即选项卡的旋转器将停止旋转,并指派onload事件后
window-all-closed 所有窗口都关闭时触发
before-quit 在关闭窗口之前触发
will-quit 在关闭窗口都已经关闭并且应用程序退出之前
quit 当所有窗口被关闭时触发
closed 当窗口关闭时发出。收到此事件后,应删除对窗口的引用,并避免再使用它

ready

1
2
3
app.on("ready", () => {
console.log("ready")
});

dom-ready

1
2
3
4
5
6
7
app.on("ready", () => {
console.log("ready");
mainWindow.webContents.on('dom-ready', () => {
console.log("dom-ready")
//一个框架中的文本加载完成后触发该事件。
})
});

did-finish-load

1
2
3
4
5
6
7
8
9
10
app.on("ready", () => {
console.log("ready");
mainWindow.webContents.on('dom-ready', () => {
console.log("dom-ready")
//一个框架中的文本加载完成后触发该事件。
})
mainWindow.webContents.on("did-finish-load", () => {
console.log("did-finish-load") //导航完成时触发,即选项卡的旋转器将停止旋转,并指派onload事件后。
})
});

window-all-closed

1
2
3
4
app.on('window-all-closed', () => { //当所有窗口被关闭时触发
console.log("window-all-closed")
app.quit()
})

before-quit

1
2
3
4
app.on('before-quit', () => {
console.log("before-quit")
//在关闭窗口之前触发
})

will-quit

1
2
3
4
app.on('will-quit', () => {
console.log("will-quit")
//在关闭窗口都已经关闭并且应用程序退出时
})

quit

1
2
3
4
5
app.on("quit", () => {
console.log("quit")
globalShortcut.unregister("ctrl+g");
globalShortcut.unregisterAll();
});

closed

1
2
3
4
mainWindow.on("closed", () => {
console.log('closed')
mainWindow = null;
});

窗口

窗口尺寸

1
2
3
4
5
6
7
8
app.whenReady().then(() => {
const mainWindow = new BrowserWindow({
x: 100,
y: 100,
width: 800,
height: 600,
})
})

x:距离屏幕左上角横向距离

y:距离屏幕左上角纵向距离

widthheight:窗口的宽度、高度

maxWidthmaxHeightminWidthminHeight:窗口的最大、最小宽度、高度

showtrue/false,窗口创建后是否直接显示

resizabletrue/false,是否能够调整大小

autoHideMenuBar:隐藏默认的菜单栏

frame:是否显示非客户区

title:如果index.html里没有设置title则使用这个

icon.ico图标的相对路径

transparent:窗口是否全透明(需要搭配frame: false使用)

因为创建好窗口后再加载index.html需要一段时间,所以先让窗口隐藏,在程序加载完html后再展示出来,这样就不会有首页白屏的现象出现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.whenReady().then(() => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
})
mainWindow.loadFile('index.html')
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
mainWindow.on('closed', () => {
console.log('mainWindow closed')
})
})

创建新窗口

这里采取一个点击一个按钮,弹出一个新窗口的方式实现

首先,优化一下目录结构,采用与微信小程序相同的目录结构,方便管理

image-20250124145917967

首先在main.js中添加一个按钮

1
<button id="openNeWindow">打开一个新窗口</button>

在index.js中添加对按钮的监听事件

1
2
3
4
5
6
window.addEventListener('DOMContentLoaded',() => {
const btn = document.getElementById('openNeWindow')
btn.addEventListener('click',() => {
/////////////////////////////
})
})

那么现在我想在这个index.js中,像主进程main.js那样创建一个窗口,应该来说也是和main.js一样,用require导入electron就可以了,那么导入一下试试

1
const o = require('electron')

ctrl + shift + I打开调试,可以看到require是没有被定义的,所以这也说明了在渲染进程中是没法直接使用nodejs的,这也是Electron出于安全性的考虑

image-20250124150735680

所以这就需要我们的main.js允许一下我的渲染进程也可以使用nodejs,在创建窗口时有一些属性,可以添加一个webPreferences,然后在里面添加nodeIntegration: true

1
2
3
4
5
6
7
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
}
});

此时require是没问题了,但是添加了以下代码仍然没法创建窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
window.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('openNeWindow')
btn.addEventListener('click', () => {
const win = new BrowserWindow({
width: 800,
height: 600,
})
win.loadURL('pages/homepage/homepage.html')
})
win.on('closed', () => {
win = null
})
})

这也是Electron基于安全性的考虑,渲染进程是没法直接用主进程的一些模块,那么就需要用到remote模块,在main.js中添加enableRemoteModule: true,然后在渲染进程index.js中导入模块时直接解构出remote就可以了,然后创建窗口时使用remote.BrowserWindow来创建

完整的index.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { remote } = require('electron')

window.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('openNeWindow')
btn.addEventListener('click', () => {
const win = new remote.BrowserWindow({
width: 800,
height: 600,
})
win.loadURL('pages/homepage/homepage.html')
})

win.on('closed', () => {
win = null // 释放
})
})

完整的main.js如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const { app, BrowserWindow } = require('electron')

app.whenReady().then(() => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
nodeIntegration: true,
enableRemoteModule: true,
}
});
mainWindow.loadFile('pages/index/index.html')
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})
mainWindow.on('closed', () => {
console.log('mainWindow closed')
})
})

app.on('window-all-closed', () => {
console.log('app window-all-closed')
app.quit()
})

但是从Electron14开始,enableRemoteModule选项已被移出,remote模块已经被完全弃用了

因此,如果你正在使用 Electron 14 或更高版本,你需要使用 contextBridge 和 ipcRenderer 来替代 remote 模块的功能。

transparent属性的bug

bug简介

创建窗口时会有一个transparent属性,如果设置为true,则会让整个应用中没有元素的地方都变成透明的(但是鼠标点击穿透不了)

但是此时如果你添加了一个按钮用来最大化窗口(maximizeunmaximize)你会发现你能够最大化(最大化窗口)但是没法恢复(窗口模式),这貌似是Electron自古以来的bug。也就是说,如果打印mainWindow.isMaximized()这个东西他会一直返回false,所以mainWindow的transparent设置为true后,窗口会被认为一直认为是没有最大化状态。并且此时调用mainWindow.unmaximize()是完全没有用的

想出来的解决方法

我自己记录是否最大化了,然后最大化的时候还是调用mainWindow.maximize()方法,unmaximize的时候直接重启一个窗口不就完了

首先我创建的这个程序本身不是一开始就最大化的,所以,先定义一个全局变量let ism = 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
let mainWindow = null // 这里记录一下,因为checkWindowScreen()函数要传入窗口
let xx = 0 // 记录之前程序所在x位置(因为可能涉及多显示器)
let yy = 0 // 记录之前程序所在y位置

function isPointInBounds(x, y, bounds) { // checkWindowScreen() 的辅助函数
return x >= bounds.x && x <= bounds.x + bounds.width &&
y >= bounds.y && y <= bounds.y + bounds.height;
}
function checkWindowScreen(window) {
const bounds = window.getBounds();
const displays = screen.getAllDisplays();

for (let i = 0; i < displays.length; i++) {
const display = displays[i];
if (isPointInBounds(bounds.x, bounds.y, display.bounds)) {
// console.log(`窗口当前在屏幕 ${i + 1}`);
xx = bounds.x; // 记录窗口在屏幕上的x坐标
yy = bounds.y; // 记录窗口在屏幕上的y坐标
window.screenIndex = i; // 更新窗口所在的屏幕索引
return;
}
}
console.log('窗口不在任何已知的屏幕中'); // 窗口移出屏幕会触发这个,不过最大化的时候一定不会触发这个
}

function cw() {
const mainWindow = new BrowserWindow({
x: xx + 400,
y: yy + 300,
width: 800,
height: 600,
show: false,
autoHideMenuBar: true,
frame: false,
transparent: true,
alwaysOnTop: true,
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
}
});
return mainWindow
}

ipcMain.on('maximize', (event, value) => { //////////////// 渲染进程告诉主进程要maximize或unmaximize了
console.log(mainWindow.isMaximized())
if (!ism) {
ism = true
mainWindow.maximize() // 这个方法还可以使用
mainWindow.resizable = false
mainWindow.movable = false // 需要将resizable属性和movable属性设置成fasle,不然最大化之后仍然能拖动这个窗口或放大缩小,那样的话还得判断是否改变了大小,再通知渲染进程修改右上角图标,更麻烦。不想这样也可以,自己实现一个监听mainWindow('move' => {})方法就可以,然后处理程序在屏幕上的位置,也能实现和普通程序本来的那种拖到最顶上最大化,拖离最顶部还原的操作,不过没(lan)必(de)要(zuo)
event.reply('receive-maximize-message', true) // 这里用来回复渲染进程改变图标
} else {
// mainWindow.unmaximize() // 这个方法不能用
ism = false
mainWindow.close() // 关闭当前窗口 // 不要使用app.quit()退出整个程序
mainWindow = cw() // 这个函数用来创建新窗口 **上面有定义**
mainWindow.loadFile('pages/index/index.html')
mainWindow.once('ready-to-show', () => {
mainWindow.show() // 减少主页白屏概率
})
mainWindow.on('maximize', () => {
checkWindowScreen(mainWindow) // 检查当前程序在哪一个显示器上 **上面有定义** 这个函数不仅在这里要监听,在程序第一次打开创建mianWindow时也要监听,也就是在这个ipc外面的那个mainWindow也要监听`maximize`
})
event.reply('receive-maximize-message', false) // 告诉渲染进程创建成功了,可以进行一些后续操作
}
})

以上代码就是最小化的时候直接重启一个窗口,不足就是肯定不连贯,毕竟关闭了一个窗口,因为涉及到多显示器判断,所以这段代码看起来比较长,你如果懒的判断多显示器,直接关了重开就行了

再就是你肯定会问:重新打开之后肯定不是上个页面所在的位置啊,比如一个设置页,我最大化的时候再第三个选项卡,但是重启了肯定就回到了刚开始的第一个啊

这个咋解决呢,那就只能在渲染进程的index.html加载的时候mainWindow.loadFile('pages/index/index.html'),在后面加上?page=1&abcabc=xxx之类的,再跳转过去,然后index.js中再解析一下参数,切换一下选项卡或标签页之类的(我也没具体试过,毕竟没这个需求,只是想到了这个方法,不过应该能实现)

当然,这个方法确实很麻烦,但对于Electron自身的这个bug来说,目前应该只能这样解决了,上网也没找到很好的方法。

下面再讲一下设置了transparent后鼠标穿透如何解决

transparent后鼠标穿透

如果单纯的设置了transparent属性,鼠标是会被Electron的这个Application捕获到的,无法直接穿过Electron的这个程序点击到下面的其他东西,当然我们肯定是希望能够点到透明后边的东西

有这么一个属性:

1
mainWindow.setIgnoreMouseEvents(true, { forward: true })

他会让整个Electron应用忽略全部鼠标操作,完全实现鼠标穿透,但这肯定也不是我们想要的。所以就判断一下什么时候鼠标穿透,什么时候不穿透

这个操作我会用一下一个例子实现(gif加载不出来就等会 ~):

789456123

学习这部分之前请先了解下面的IPC通信 相关内容

首先,创建mainWindow的时候,应有以下属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const mainWindow = new BrowserWindow({
x: xx + 400,
y: yy + 300,
// xx和yy是解决上述transparent bug时添加的,如果不需要解决上述transparent的bug则可以直接删除x和y,直接让窗口创建在屏幕中间
width: 800,
height: 600,
autoHideMenuBar: true,
frame: false, // 这里使用自定义非客户区
transparent: true, // 背景透明
alwaysOnTop: true, // 置顶窗口
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
}
});

然后我们来进行后续操作

首先我在preload.js中建立一个ipc通信,函数名使用IgnoreMouse的缩写代替了

1
2
3
4
contextBridge.exposeInMainWorld('CustomAPI', {
IgnoreM:() => ipcRenderer.send('IgnoreM'),
unIgnoreM:() => ipcRenderer.send('unIgnoreM')
})

main.js绑定这个通信:

1
2
3
4
5
6
7
ipcMain.on('IgnoreM', (event, value) => {
mainWindow.setIgnoreMouseEvents(true, { forward: true })
})

ipcMain.on('unIgnoreM', (event, value) => {
mainWindow.setIgnoreMouseEvents(false)
})

看一下我的index.html的主要部分,非客户区是我自己定义的,比较长一坨

image-20250126130348407

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
<body>
<!-- Custom Title Bar ############## 非客户区-->
<div id="draggableBar">
<div id="left-box">
<img src="../../src/icon.jpg">
<div>Creeeeeeeeeeper.exe</div>
</div>

<div id="right-box">
<div id="close">
<svg t="1737724919494" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1596" width="13.5" height="13.5">
<path
d="M556.8 512l281.6 281.6c12.8 12.8 12.8 32 0 44.8s-32 12.8-44.8 0L512 556.8l-281.6 281.6c-12.8 12.8-32 12.8-44.8 0s-12.8-32 0-44.8l281.6-281.6L185.6 230.4c-12.8-12.8-12.8-32 0-44.8s32-12.8 44.8 0l281.6 281.6 281.6-281.6c12.8-12.8 32-12.8 44.8 0s12.8 32 0 44.8L556.8 512z"
p-id="1597" fill="#8a8a8a"></path>
</svg>
</div>
<div id="maxsize">
<svg t="1737720715887" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="5182" width="13" height="13">
<path
d="M810.666667 938.666667h-128c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h128c25.6 0 42.666667-17.066667 42.666666-42.666666v-128c0-25.6 17.066667-42.666667 42.666667-42.666667s42.666667 17.066667 42.666667 42.666667v128c0 72.533333-55.466667 128-128 128zM341.333333 938.666667H213.333333c-72.533333 0-128-55.466667-128-128v-128c0-25.6 17.066667-42.666667 42.666667-42.666667s42.666667 17.066667 42.666667 42.666667v128c0 25.6 17.066667 42.666667 42.666666 42.666666h128c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667zM896 384c-25.6 0-42.666667-17.066667-42.666667-42.666667V213.333333c0-25.6-17.066667-42.666667-42.666666-42.666666h-128c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h128c72.533333 0 128 55.466667 128 128v128c0 25.6-17.066667 42.666667-42.666667 42.666667zM128 384c-25.6 0-42.666667-17.066667-42.666667-42.666667V213.333333c0-72.533333 55.466667-128 128-128h128c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667H213.333333c-25.6 0-42.666667 17.066667-42.666666 42.666666v128c0 25.6-17.066667 42.666667-42.666667 42.666667z"
fill="#8a8a8a" p-id="5183"></path>
</svg>
</div>
<div id="minisize">
<svg t="1737720624130" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="3308" width="12.5" height="32">
<path
d="M930.355 551.424H94.218c-23.987 0-43.975-17.408-43.975-39.424 0-21.504 19.42-39.424 43.975-39.424h835.564c23.987 0 43.975 17.408 43.975 39.424 0.006 22.016-19.415 39.424-43.402 39.424z"
fill="#8a8a8a" p-id="3309"></path>
</svg>
</div>
</div>
</div>

<!-- Main Content ################ 这里是客户区,也就是所有半透明的部分,所有的东西都要写在这个div里面-->
<div class="blurBackground" id="IgnoreBackground">
<button class="chd" onclick="openNewWindow()">打开新窗口</button>
<button class="chd" onclick="openNewWindow()">打开新窗口</button>
<div class="chd" style="width: 200px; height: 100px; background-color: green; display: flex; flex-direction: column;">
<button onclick="openNewWindow()">打开新窗口</button>
<button onclick="openNewWindow()">打开新窗口</button>
</div>
</div>
</body>

index.html:首先要给整个客户区绑定一个id: IgnoreBackground

在index.js中监听这个区域:鼠标移入非客户区的时候(标题栏)时不忽略鼠标,也就是让鼠标还能在标题栏中进行各种操作(拖动窗口或点击右侧按钮),鼠标移入客户区 (id=”IgnoreBackground”) 的时候,向主进程通信,忽略鼠标

1
2
3
4
5
6
7
8
9
10
11
12
document.getElementById('IgnoreBackground').addEventListener('mouseenter', (event) => {
if (event.target.id === 'IgnoreBackground') {
window.CustomAPI.IgnoreM()
// console.log('IgnoreBackground')
}
})
document.getElementById('IgnoreBackground').addEventListener('mouseleave', (event) => {
if (event.target.id === 'IgnoreBackground') {
window.CustomAPI.unIgnoreM()
// console.log('unIgnoreBackground')
}
})

现在基本功能完成了,但是鼠标移入到客户区 (id=”IgnoreBackground”) 的时候,里面的子元素没法点击啊,鼠标仍然被判断为没有移出客户区

这时候就给客户区中的所有下一级子元素绑定一个class(也就是上面html中的class=”chd”),然后给所有有这个class的子元素监听mouseovermouseleave,然后阻止子元素事件冒泡到父元素

1
2
3
4
5
6
7
8
9
10
11
const elm = document.getElementsByClassName('chd')
for (let i = 0; i < elm.length; i++) {
elm[i].addEventListener('mouseover', (event) => {
event.stopPropagation()
window.CustomAPI.unIgnoreM()
})
elm[i].addEventListener('mouseleave', (event) => {
event.stopPropagation()
window.CustomAPI.IgnoreM()
})
}

PS: 如果客户区中子元素非常多,那么建议使用js循环给子元素添加class

PS: 不会区分mouseentermouseleavemouseovermouseout四者的区别先自行百度bing一下

设置完这一堆之后,就能在透明的地方实现鼠标穿透点击等操作,在绑定了class=”chd”的地方仍然能对Electron程序中的元素进行操作

主题切换

这个东西比较简单,只把主要代码放在这里了,可以抄一下(以下是完整代码,所以比较长)

darkmode
main.js
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
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('node:path')
const fs = require('fs')


function createWindow() {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
minWidth: 450,
frame: false,
autoHideMenuBar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.js')
}
})

return mainWindow
}

app.whenReady().then(() => {

var mainWindowHandle = createWindow()
mainWindowHandle.loadFile('pages/index/index.html')
mainWindowHandle.once('ready-to-show', () => {
mainWindowHandle.show()
})

ipcMain.on('maximize', (event, value) => {
if (!mainWindowHandle.isMaximized()) {
mainWindowHandle.maximize()
event.reply('receive-maximize-message', true)
} else {
mainWindowHandle.unmaximize()
event.reply('receive-maximize-message', false)
}
})

ipcMain.on('minimize', (event, value) => {
mainWindowHandle.minimize()
})

ipcMain.on('close', (event, value) => {
mainWindowHandle.close()
})

mainWindowHandle.on('maximize', () => {
mainWindowHandle.webContents.send('window-maximized', 'Window is maximized');
})
mainWindowHandle.on('unmaximize', () => {
mainWindowHandle.webContents.send('window-unmaximized', 'Window is unmaximized');
});
})

app.on('window-all-closed', () => {
app.quit()
})

app.on('dom-ready', () => {
document.addEventListener('contextmenu', function (event) {
event.preventDefault();
}, false);
})
index.html
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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="index.css">
</head>

<body>
<!-- Custom Title Bar -->
<div id="draggableBar">
<div id="left-box">
<img id="iconpic" class="light-theme-shadow" src="../../src/icon.png">
<div id="tt" style="margin-left: 2px;">Data Analyser</div>
</div>

<div id="right-box">
<div id="close">
<svg t="1737724919494" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="1596" width="13.5" height="13.5">
<path
d="M556.8 512l281.6 281.6c12.8 12.8 12.8 32 0 44.8s-32 12.8-44.8 0L512 556.8l-281.6 281.6c-12.8 12.8-32 12.8-44.8 0s-12.8-32 0-44.8l281.6-281.6L185.6 230.4c-12.8-12.8-12.8-32 0-44.8s32-12.8 44.8 0l281.6 281.6 281.6-281.6c12.8-12.8 32-12.8 44.8 0s12.8 32 0 44.8L556.8 512z"
p-id="1597" fill="#8a8a8a"></path>
</svg>
</div>
<div id="maxsize">
<svg t="1737720715887" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="5182" width="13" height="13">
<path
d="M810.666667 938.666667h-128c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h128c25.6 0 42.666667-17.066667 42.666666-42.666666v-128c0-25.6 17.066667-42.666667 42.666667-42.666667s42.666667 17.066667 42.666667 42.666667v128c0 72.533333-55.466667 128-128 128zM341.333333 938.666667H213.333333c-72.533333 0-128-55.466667-128-128v-128c0-25.6 17.066667-42.666667 42.666667-42.666667s42.666667 17.066667 42.666667 42.666667v128c0 25.6 17.066667 42.666667 42.666666 42.666666h128c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667zM896 384c-25.6 0-42.666667-17.066667-42.666667-42.666667V213.333333c0-25.6-17.066667-42.666667-42.666666-42.666666h-128c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h128c72.533333 0 128 55.466667 128 128v128c0 25.6-17.066667 42.666667-42.666667 42.666667zM128 384c-25.6 0-42.666667-17.066667-42.666667-42.666667V213.333333c0-72.533333 55.466667-128 128-128h128c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667H213.333333c-25.6 0-42.666667 17.066667-42.666666 42.666666v128c0 25.6-17.066667 42.666667-42.666667 42.666667z"
fill="#8a8a8a" p-id="5183"></path>
</svg>
</div>
<div id="minisize">
<svg t="1737720624130" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="3308" width="12.5" height="32">
<path
d="M930.355 551.424H94.218c-23.987 0-43.975-17.408-43.975-39.424 0-21.504 19.42-39.424 43.975-39.424h835.564c23.987 0 43.975 17.408 43.975 39.424 0.006 22.016-19.415 39.424-43.402 39.424z"
fill="#8a8a8a" p-id="3309"></path>
</svg>
</div>
<div id="theme">
<svg style="margin-top: 1px;" t="1740838433864" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="3190" width="17.5" height="17.5">
<path
d="M549.302857 84.297143c0-20.150857-17.152-37.302857-37.302857-37.302857s-37.284571 17.152-37.284571 37.302857V174.262857c0 20.150857 17.152 37.302857 37.302857 37.302857 20.114286 0 37.266286-17.152 37.266285-37.302857z m174.409143 163.273143c-14.134857 14.573714-14.134857 38.582857 0 52.717714 14.573714 14.573714 38.144 14.994286 53.138286 0l63.872-63.853714a37.76 37.76 0 0 0 0-53.156572c-14.153143-14.134857-38.144-14.134857-52.717715 0z m-476.562286 52.717714c14.134857 14.573714 38.144 14.573714 52.699429 0 14.153143-13.714286 14.153143-38.582857 0.438857-52.717714l-63.853714-64.292572c-13.714286-13.714286-38.144-14.134857-52.717715 0-14.153143 14.153143-14.153143 38.582857-0.438857 52.717715zM512 293.430857c-119.570286 0-218.569143 98.998857-218.569143 218.569143S392.411429 731.008 512 731.008c119.149714 0 218.148571-99.437714 218.148571-219.008 0-119.588571-98.998857-218.569143-218.148571-218.569143z m0 65.572572c83.565714 0 152.996571 69.430857 152.996571 152.996571S595.565714 665.417143 512 665.417143c-84.004571 0-153.417143-69.851429-153.417143-153.417143s69.412571-152.996571 153.417143-152.996571z m426.422857 190.281142c20.150857 0 37.302857-17.133714 37.302857-37.284571s-17.152-37.302857-37.302857-37.302857h-89.563428c-20.150857 0-37.302857 17.152-37.302858 37.302857s17.152 37.284571 37.302858 37.284571zM85.577143 474.715429c-20.150857 0-37.302857 17.133714-37.302857 37.284571s17.152 37.284571 37.302857 37.284571h89.563428c20.150857 0 37.302857-17.133714 37.302858-37.284571s-17.152-37.302857-37.302858-37.302857zM776.411429 724.114286a38.034286 38.034286 0 0 0-52.717715 0c-14.134857 14.153143-14.134857 38.162286 0 52.717714L788.004571 841.142857c14.573714 14.134857 38.582857 13.714286 52.717715-0.420571 14.573714-14.153143 14.573714-38.144 0-52.717715z m-593.152 63.451428c-14.555429 14.134857-14.555429 38.564571-0.420572 52.699429 14.153143 14.153143 38.582857 14.573714 53.138286 0.438857l63.853714-63.872c14.153143-14.134857 14.153143-38.144 0.438857-52.699429-14.153143-14.153143-38.582857-14.153143-53.138285 0z m366.006857 62.134857c0-20.150857-17.133714-37.302857-37.284572-37.302857s-37.284571 17.152-37.284571 37.302857v90.002286c0 20.132571 17.152 37.284571 37.302857 37.284572 20.114286 0 37.266286-17.152 37.266286-37.302858z"
p-id="3191" data-spm-anchor-id="a313x.search_index.0.i0.227f3a81VKAdac" class="selected"
fill="#8a8a8a"></path>
</svg>
</div>
</div>
</div>
<div class="content">


</div>
<script type="module" src="index.js"></script>
</body>

</html>
index.css
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
126
127
128
129
130
131
132
body {
margin: 0;
padding: 0;
white-space: nowrap;
height: 100vh;
display: flex;
flex-direction: column;
border-radius: 20px;
transition: background-color 0.5s;
}

.content {
flex-grow: 1;
}

#draggableBar {
background-color: rgb(243, 243, 243);
height: 30px;
width: 100%;
display: flex;
flex-direction: row;
transition: background-color 0.5s;
}


#left-box {
height: 100%;
flex-grow: 1;
min-width: 50px;
margin-left: 0px;
display: flex;
margin-top: 0px;
align-items: center;
-webkit-app-region: drag;
transition: background-color 0.5s;
}

#left-box img {
height: 25px;
width: 25px;
margin-left: 5px;
border-radius: 12.5px;
}

.light-theme-shadow {
box-shadow: -2px 2px 3px rgba(0, 0, 0, 0.2);
}

.dark-theme-shadow {
box-shadow: -2px 2px 3px rgba(255, 255, 255, 0);
}

#left-box div {
margin-left: 0px;
white-space: nowrap;
font-size: 12.5px;
}

#right-box {
height: 100%;
/* width: 30%; */
display: flex;
flex-direction: row-reverse;
transition: background-color 0.5s;
}

#right-box div {
height: 100%;
width: 200px;
max-width: 50px;
min-width: 50px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
}

#close:hover {
background-color: rgb(232, 17, 35);
}

#close:hover svg path {
fill: rgb(255, 255, 255);
}

#minisize:hover {
background-color: rgba(160, 160, 160, 0.2);
}

#maxsize:hover {
background-color: rgba(160, 160, 160, 0.2);
}

#theme:hover {
background-color: rgba(160, 160, 160, 0.2);
}

.icon {
pointer-events: none;
}

.blurBackground {
top: 0;
left: 0;
width: 100%;
height: 100%;
flex: 1;
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}

.dark-theme {
background-color: #323233;
}

.dark-theme-body {
background-color: #252525;
}

#tt {
transition: background-color 0.5s;
}

.dark-theme-tt {
color: #8a8a8a;
}

.svg path {
fill: #cccccc;
}
index.js
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
// add event listener to buttons
window.addEventListener('DOMContentLoaded', () => {
// minimize button
document.getElementById('minisize').addEventListener('click', (event) => {
event.stopPropagation()
window.FunctionBar.minimize()
})
// maximize button
document.getElementById('maxsize').addEventListener('click', (event) => {
event.stopPropagation()
window.FunctionBar.maximize()
})
// close button
document.getElementById('close').addEventListener('click', (event) => {
event.stopPropagation()
window.FunctionBar.close()
})
// theme button
document.getElementById('theme').addEventListener('click', (event) => {
event.stopPropagation()
toggleTheme()
})
})

window.FunctionBar.reply('receive-maximize-message', (event, message) => {
if (message) {
setMaximizeIcon()
} else {
setMinimizeIcon()
}
})

window.maxi.onMessage('window-maximized', (arg) => {
setMaximizeIcon()
})

window.maxi.onMessage('window-unmaximized', (arg) => {
setMinimizeIcon()
})

function setMaximizeIcon() {
document.getElementById('maxsize').innerHTML = `<svg t="1737720796675" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="7019" width="13" height="13">
<path
d="M682.666667 938.666667c-25.6 0-42.666667-17.066667-42.666667-42.666667v-128c0-72.533333 55.466667-128 128-128h128c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666666h-128c-25.6 0-42.666667 17.066667-42.666667 42.666667v128c0 25.6-17.066667 42.666667-42.666666 42.666667z m-341.333334 0c-25.6 0-42.666667-17.066667-42.666666-42.666667v-128c0-25.6-17.066667-42.666667-42.666667-42.666667H128c-25.6 0-42.666667-17.066667-42.666667-42.666666s17.066667-42.666667 42.666667-42.666667h128c72.533333 0 128 55.466667 128 128v128c0 25.6-17.066667 42.666667-42.666667 42.666667zM896 384h-128c-72.533333 0-128-55.466667-128-128V128c0-25.6 17.066667-42.666667 42.666667-42.666667s42.666667 17.066667 42.666666 42.666667v128c0 25.6 17.066667 42.666667 42.666667 42.666667h128c25.6 0 42.666667 17.066667 42.666667 42.666666s-17.066667 42.666667-42.666667 42.666667zM256 384H128c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666666h128c25.6 0 42.666667-17.066667 42.666667-42.666667V128c0-25.6 17.066667-42.666667 42.666666-42.666667s42.666667 17.066667 42.666667 42.666667v128c0 72.533333-55.466667 128-128 128z"
p-id="7020" fill="#8a8a8a"></path>
</svg>`
}

function setMinimizeIcon() {
document.getElementById('maxsize').innerHTML = `<svg t="1737720715887" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="5182" width="13" height="13">
<path
d="M810.666667 938.666667h-128c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h128c25.6 0 42.666667-17.066667 42.666666-42.666666v-128c0-25.6 17.066667-42.666667 42.666667-42.666667s42.666667 17.066667 42.666667 42.666667v128c0 72.533333-55.466667 128-128 128zM341.333333 938.666667H213.333333c-72.533333 0-128-55.466667-128-128v-128c0-25.6 17.066667-42.666667 42.666667-42.666667s42.666667 17.066667 42.666667 42.666667v128c0 25.6 17.066667 42.666667 42.666666 42.666666h128c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667zM896 384c-25.6 0-42.666667-17.066667-42.666667-42.666667V213.333333c0-25.6-17.066667-42.666667-42.666666-42.666666h-128c-25.6 0-42.666667-17.066667-42.666667-42.666667s17.066667-42.666667 42.666667-42.666667h128c72.533333 0 128 55.466667 128 128v128c0 25.6-17.066667 42.666667-42.666667 42.666667zM128 384c-25.6 0-42.666667-17.066667-42.666667-42.666667V213.333333c0-72.533333 55.466667-128 128-128h128c25.6 0 42.666667 17.066667 42.666667 42.666667s-17.066667 42.666667-42.666667 42.666667H213.333333c-25.6 0-42.666667 17.066667-42.666666 42.666666v128c0 25.6-17.066667 42.666667-42.666667 42.666667z"
fill="#8a8a8a" p-id="5183"></path>
</svg>`
}
var theme = false
function toggleTheme() {
classToggle('draggableBar', 'dark-theme')
classToggle('left-box', 'dark-theme')
classToggle('right-box', 'dark-theme')
classToggle('close', 'dark-theme')
classToggle('maxsize', 'dark-theme')
classToggle('minisize', 'dark-theme')
classToggle('theme', 'dark-theme')
classToggle('tt', 'dark-theme-tt')
classToggle('iconpic', 'light-theme-shadow')
classToggle('iconpic', 'dark-theme-shadow')
document.body.classList.toggle('dark-theme-body')
if (!theme) {
theme = true
document.getElementById('theme').innerHTML = `<svg t="1740885248898" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="5947" width="15" height="15">
<path
d="M558.27 1024c157.512 0 301.964-71.608 397.688-189.55 56.54-69.662-5.116-171.444-92.498-154.802-164.696 31.366-316.544-94.536-316.544-261.584 0-96.848 52.12-184.584 134.868-231.672 77.49-44.1 57.998-161.576-30.044-177.838A515.872 515.872 0 0 0 558.27 0c-282.72 0-512 229.15-512 512 0 282.72 229.152 512 512 512z m0-928c25.97 0 51.378 2.402 76.032 6.956-109.52 62.326-183.386 180.084-183.386 315.108 0 227.696 207.282 398.4 430.504 355.888C805.148 867.928 688.732 928 558.27 928c-229.75 0-416-186.25-416-416s186.25-416 416-416z"
fill="#8a8a8a" p-id="5948"></path>
</svg>`
} else {
theme = false
document.getElementById('theme').innerHTML = `<svg style="margin-top: 1px" t="1740838433864" class="icon" viewBox="0 0 1024 1024" version="1.1"
xmlns="http://www.w3.org/2000/svg" p-id="3190" width="17.5" height="17.5">
<path
d="M549.302857 84.297143c0-20.150857-17.152-37.302857-37.302857-37.302857s-37.284571 17.152-37.284571 37.302857V174.262857c0 20.150857 17.152 37.302857 37.302857 37.302857 20.114286 0 37.266286-17.152 37.266285-37.302857z m174.409143 163.273143c-14.134857 14.573714-14.134857 38.582857 0 52.717714 14.573714 14.573714 38.144 14.994286 53.138286 0l63.872-63.853714a37.76 37.76 0 0 0 0-53.156572c-14.153143-14.134857-38.144-14.134857-52.717715 0z m-476.562286 52.717714c14.134857 14.573714 38.144 14.573714 52.699429 0 14.153143-13.714286 14.153143-38.582857 0.438857-52.717714l-63.853714-64.292572c-13.714286-13.714286-38.144-14.134857-52.717715 0-14.153143 14.153143-14.153143 38.582857-0.438857 52.717715zM512 293.430857c-119.570286 0-218.569143 98.998857-218.569143 218.569143S392.411429 731.008 512 731.008c119.149714 0 218.148571-99.437714 218.148571-219.008 0-119.588571-98.998857-218.569143-218.148571-218.569143z m0 65.572572c83.565714 0 152.996571 69.430857 152.996571 152.996571S595.565714 665.417143 512 665.417143c-84.004571 0-153.417143-69.851429-153.417143-153.417143s69.412571-152.996571 153.417143-152.996571z m426.422857 190.281142c20.150857 0 37.302857-17.133714 37.302857-37.284571s-17.152-37.302857-37.302857-37.302857h-89.563428c-20.150857 0-37.302857 17.152-37.302858 37.302857s17.152 37.284571 37.302858 37.284571zM85.577143 474.715429c-20.150857 0-37.302857 17.133714-37.302857 37.284571s17.152 37.284571 37.302857 37.284571h89.563428c20.150857 0 37.302857-17.133714 37.302858-37.284571s-17.152-37.302857-37.302858-37.302857zM776.411429 724.114286a38.034286 38.034286 0 0 0-52.717715 0c-14.134857 14.153143-14.134857 38.162286 0 52.717714L788.004571 841.142857c14.573714 14.134857 38.582857 13.714286 52.717715-0.420571 14.573714-14.153143 14.573714-38.144 0-52.717715z m-593.152 63.451428c-14.555429 14.134857-14.555429 38.564571-0.420572 52.699429 14.153143 14.153143 38.582857 14.573714 53.138286 0.438857l63.853714-63.872c14.153143-14.134857 14.153143-38.144 0.438857-52.699429-14.153143-14.153143-38.582857-14.153143-53.138285 0z m366.006857 62.134857c0-20.150857-17.133714-37.302857-37.284572-37.302857s-37.284571 17.152-37.284571 37.302857v90.002286c0 20.132571 17.152 37.284571 37.302857 37.284572 20.114286 0 37.266286-17.152 37.266286-37.302858z"
p-id="3191" data-spm-anchor-id="a313x.search_index.0.i0.227f3a81VKAdac" class="selected"
fill="#8a8a8a"></path>
</svg>`
}
}

function classToggle(element, className) {
document.getElementById(element).classList.toggle(className)
}

preload.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('FunctionBar', {
maximize: () => ipcRenderer.send('maximize'),
minimize: () => ipcRenderer.send('minimize'),
close: () => ipcRenderer.send('close'),
reply: (channel, callback) => ipcRenderer.on(channel, callback)
})
contextBridge.exposeInMainWorld('maxi', {
onMessage: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
},
sendToMain: (channel, data) => {
ipcRenderer.send(channel, data);
}
});

IPC通信

为什么有IPC(InterProcess Communication)通信?

如果你的渲染进程加载了第三方js或者js是cdn,这时候如果不对渲染进程进行限制,那么就可能直接调用node.js,node.js又能直接操作系统API,就可能造成安全问题

Electron IPC通信过程:通过一个preload.js预加载脚本建立IPC通道实现通信

原理和简单的代码实现(渲染进程通知主进程创建一个窗口,主进程返回一个res)如下:

渲染进程向主进程通信

image-20250124190443382

mian.js

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
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')

app.whenReady().then(() => {
const mainWindow = new BrowserWindow({
width: 800,
height: 600,
show: false,
autoHideMenuBar: true,
frame: true,
transparent: false,
webPreferences: {
nodeIntegration: true,
preload: path.join(__dirname, 'preload.js'),
}
});
mainWindow.loadFile('pages/index/index.html')
mainWindow.once('ready-to-show', () => {
mainWindow.show()
})

mainWindow.on('closed', () => {
console.log('mainWindow closed')
})

ipcMain.on('createWindow', (event, value) => {
const { x, y, width, height, pageurl } = value
const newWindow = new BrowserWindow({
x: x,
y: y,
width: width,
height: height,
})
newWindow.loadFile(pageurl)
newWindow.once('ready-to-show', () => {
newWindow.show()
})
})

app.on('window-all-closed', () => {
console.log('app window-all-closed')
app.quit()
})
})

preload.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('createWindow', {
createWd: (x, y, width, height, pageurl) => {
ipcRenderer.send('createWindow', {
x: x,
y: y,
width: width,
height: height,
pageurl: pageurl
})
}
})

index.js

1
2
3
4
5
6
window.addEventListener('DOMContentLoaded', () => {
const btn = document.getElementById('openNeWindow')
btn.addEventListener('click', () => {
window.createWindow.createWd(500, 500, 400, 300 ,'pages/homepage/homepage.html');
})
})

更多关于进程间通信详细内容请查看 进程间通信 | Electron

主进程回复渲染进程的通信

在上面的代码中,main.js中添加event.reply就可以

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ipcMain.on('createWindow', (event, value) => {
const { x, y, width, height, pageurl } = value
const newWindow = new BrowserWindow({
x: x,
y: y,
width: width,
height: height,
})
newWindow.loadFile(pageurl)
newWindow.once('ready-to-show', () => {
newWindow.show()
})
/////////////////////////////////////////////////////////////////////////////////
if (newWindow) {
var res = {
status: 'success',
message: 'New window created successfully'
}
event.reply('xxxchanel', res) //////////////// 添加回复事件/////////////////
}
})

preload.js中添加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('createWindow', {
createWd: (x, y, width, height, pageurl) => {
ipcRenderer.send('createWindow', {
x: x,
y: y,
width: width,
height: height,
pageurl: pageurl
})
},
reply: (channel, callback) => ipcRenderer.on(channel, callback) //////////添加这里//////////
})

index.js渲染进程中添加:

1
2
3
window.createWindow.reply('xxxchanel', (event, message) => {
console.log(message);
})

确保main.js中event.reply( xxxchanel , res)的xxxchanel和index.js中的xxxchanel相同,主进程即可向渲染进场回复

主进程主动向渲染进程通信

比如我在实现窗口maximizeunmaximize的时候,我不使用系统默认的最大化图标,而是完全自定义,那么我如果不点击按钮进行放大,而是双击程序的标题栏,或者拖动到屏幕顶端进行最大化,那么就需要主进程检测是否最大化了,然后通知渲染进程改变最大化的图标 image-20250126113951249image-20250126114106397

main.js监听程序是否maximizedunmaximized,并分别发送到window-maximizedwindow-unmaximized的chanel

1
2
3
4
5
6
mainWindow.on('maximize', () => {
mainWindow.webContents.send('window-maximized', 'Window is maximized');
})
mainWindow.on('unmaximize', () => {
mainWindow.webContents.send('window-unmaximized', 'Window is unmaximized');
});

preload.js建立通信:

1
2
3
4
5
contextBridge.exposeInMainWorld('abcMaximize', {
onMessage: (channel, func) => {
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
});

index.js渲染进程接收消息:

1
2
3
4
5
6
7
window.abcMaximize.onMessage('window-maximized', (arg) => {
setMaximizeIcon() // 定义一个函数,改变最大化出处的icon
});

window.abcMaximize.onMessage('window-unmaximized', (arg) => {
setMinimizeIcon() // 定义一个函数,改变最大化出处的icon
});

以上三个例子说明了使用contextBridge方法进行ipc通信的示例,还有其他的方法,这里就不讲了,因为简直易(wo)如(bu)反(xiang)掌(xue)

消息通知

像Windows那样,在桌面右下角弹出一个通知窗口,点击后有相应的操作

78456

官方文档说在渲染进程中可以直接操作notification,那就在渲染进程操作吧,因为比较简单

渲染进程创建

先随便写个button

1
<button id="notificationBtn">消息通知</button>

绑定一下

1
2
3
4
5
6
7
8
9
10
11
document.getElementById('notificationBtn').addEventListener('click', (event) => {
let options = {
title: 'Notification Title',
body: 'Notification Body',
icon: '../../src/icon.jpg'
}
let n = new Notification(options.title, options)
n.onclick = () => { // 点击弹出的消息时执行的操作
console.log('Notification clicked')
}
})

主进程创建

算了,写一下吧,也不费时间

preload.js

为了方便渲染进程进行后续操作,我添加了一个rp参数(reply)作为返回值,用于渲染进程接收

1
2
3
4
5
6
7
8
9
contextBridge.exposeInMainWorld('NotificationAPI', {
Notification: (title, body, icon, rp) => ipcRenderer.send('Notification', {
title: title,
body: body,
icon: icon,
rp: rp
}),
reply: (channel, callback) => ipcRenderer.on(channel, callback)
})

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
ipcMain.on('Notification', (event, value) => {
const { title, body, icon, rp } = value;
const n = new Notification({
title: title,
body: body,
icon: icon
})
console.log(n)
n.show()
n.on('click', () => {
event.reply('notification-clicked', rp);
})
});

index.js

1
2
3
4
5
6
7
8
9
10
11
12
document.getElementById('notificationBtn').addEventListener('click', (event) => {
let title = 'Notification Title'
let body = 'Notification Body'
let icon = 'src/icon.jpg'
let rp = 3
window.NotificationAPI.Notification(title, body, icon, rp)
})

// 接收主进程返回
window.NotificationAPI.reply('notification-clicked', (event, message) => {
console.log(message)
});

点击后右下角就会弹出这个东西

image-20250126182202775


怎么办怎么办?!

就在此时,你小手一抖!不小心点上了关闭electron.app.Electron的所有通知!你火急火燎的打开了设置>系统>通知,从上到下翻了好几遍,发现没有能打开它的地方?!?!?!

image-20250126182317733

image-20250126182701312

打开C:\Users\<username>\AppData\Local\Microsoft\Windows\Notifications这个目录,里面有一个wpndatabase.db数据库,打开它,里面长这样:

image-20250126182853234

先打开NotificationHandler表,拉到最底下,找到electron的应用,因该是一找就能找到,记住前面的序号,543

image-20250126183124374

打开另一张表HandlerSettings,里面长这样:

image-20250126183243057

点击第一列HandlerId进行倒序,找到刚才对应的 ID 543

image-20250126183544394

找到这个s:toast,如果你关闭了通知,那么这里应该是0,把它改成1

image-20250126183704952

然后打开注册表,去到这个路径:计算机\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\,找到Electron的程序名,里面应该会发现多了一个Enable0的项,直接把这个项删掉

image-20250126183906306

然后重启电脑,消失的通知就又回来了!

Electron & Python

例子1

下面使用最简单的方式实现一下

首先要新建一个虚拟环境,用来指定python解释器路径

这里python程序只有一行

demo.py

1
print("Hello Electron from Python")

现在要让渲染进程监听到这个输出

main.js

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
const { exec } = require('child_process');
app.whenReady().then(() => {
ipcMain.on('executePythonCode', (event, value) => {
exec('.\\py\\Scripts\\python.exe .\\pycode\\demo.py', (error, stdout,stderr) => {
if (error) {
console.error(`执行出错: ${error.message}`);
var data = {
status: 'error',
output: error.message
}
event.reply('pyexecOutput', data)
return;
}
if (stderr) {
console.error(`错误输出: ${stderr}`);
var data = {
status: 'error',
output: stderr
}
event.reply('pyexecOutput', data)
return;
}
console.log(`标准输出: ${stdout}`);
var data = {
status:'success',
output: stdout
}
event.reply('pyexecOutput', data)
})
})
})

preload.js

1
2
3
4
contextBridge.exposeInMainWorld('pycode', {
execute: () => ipcRenderer.send('executePythonCode'),
reply: (channel, callback) => ipcRenderer.on(channel, callback)
});

index.js

1
2
3
4
5
6
7
8
function runcode() { // 绑定按钮等,用来执行
window.pycode.execute()
}

window.pycode.reply('pyexecOutput', (event, message) => {
console.log('status: ', message.status)
console.log('output: ', message.output)
})

以下结果:

image-20250302211419397

例子2

例子1中使用的child_process中的exec,这个是比较简单便捷的执行和输出方式,但是python程序如果输出相当多的东西,可能会出现output: stdout maxBuffer length exceeded的错误,也就是缓冲区的大小容纳不了python的输出,这里缓冲区的大小一般是200KB,当然,exec也可以更改缓冲区的大小,比如改成50MB:添加maxBuffer

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
ipcMain.on('executePythonCode', (event, value) => {
exec('.\\py\\Scripts\\python.exe .\\pycode\\loadExcel.py',{ maxBuffer: 1024 * 1024 * 50 }, (error, stdout,stderr) => {
if (error) {
console.error(`执行出错: ${error.message}`);
var data = {
status: 'error',
output: error.message
}
event.reply('pyexecOutput', data)
return;
}
if (stderr) {
console.error(`错误输出: ${stderr}`);
var data = {
status: 'error',
output: stderr
}
event.reply('pyexecOutput', data)
return;
}
console.log(`标准输出: ${stdout}`);
var data = {
status:'success',
output: stdout
}
event.reply('pyexecOutput', data)
})
})

但是这会导致应用占用的内存增加,更好的解决办法是使用spawn

1
const { spawn } = require('child_process');

main.js

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
ipcMain.on('executePythonCode', (event, value) => {
const pythonProcess = spawn('.\\py\\Scripts\\python.exe', ['.\\pycode\\loadExcel.py']);

pythonProcess.stdout.on('data', (data) => {
console.log(`标准输出: ${data}`);
event.reply('pyexecOutput', {
status: 'running',
output: data.toString()
});
});

pythonProcess.stderr.on('data', (data) => {
console.error(`错误输出: ${data}`);
event.reply('pyexecOutput', {
status: 'error',
output: data.toString()
});
});

pythonProcess.on('close', (code) => {
console.log(`子进程退出,退出码 ${code}`);
event.reply('pyexecOutput', {
status: code === 0 ? 'success' : 'error',
output: code === 0 ? '执行完成' : '执行出错'
});
});

pythonProcess.on('error', (error) => {
console.error(`执行出错: ${error.message}`);
event.reply('pyexecOutput', {
status: 'error',
output: error.message
});
});
});

这样就会python边输出,main.js边获取

效果如下:

有一个python.py

1
2
3
print("Hello Electron from Python")
for i in range(1000000):
print(i)

index.js要在页面上渲染所有输出内容

1
2
3
4
window.pycode.reply('pyexecOutput', (event, message) => {
console.log('status: ', message.status)
document.getElementById('output').innerHTML += message.output + '<br>'
})
image-20250303102545120

但是由于执行速度和获取速度问题,main.js可能不是按照相同的数据量获取数据,所以在上方呈现的就是每一行输出的数量不一样

为了解决这种问题,提供以下解决办法:

  • Python分块输出:修改Python脚本,让其使用某种方式(比如特殊的字符串标记)来分割数据块,然后让main.js或index.js来识别标记并处理每个数据块
  • 分页处理:可以考虑在Python中添加分页逻辑,当main.js或index.js获取完一页的数据后再请求下一页数据,这种方法可能会有点慢,适合数据量不是很大的数据

还有一种解决办法是让main.js接收数据时,每次接收相同数据量大小的块,比如以下代码,每次接收100KB

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
ipcMain.on('executePythonCode', (event, value) => {
const pythonProcess = spawn('.\\py\\Scripts\\python.exe', ['.\\pycode\\loadExcel.py']);

let outputBuffer = '';
const chunkSize = 1024 * 100; // 每次接收的数据量,100KB/////////////////////////////////////////

pythonProcess.stdout.on('data', (data) => {
outputBuffer += data.toString();

// 当缓冲区的数据量达到或超过 chunkSize 时,发送数据
while (outputBuffer.length >= chunkSize) {
const chunk = outputBuffer.substring(0, chunkSize);
outputBuffer = outputBuffer.substring(chunkSize);

const dataToSend = {
status: 'success',
output: chunk
};
event.reply('pyexecOutput', dataToSend);
}
});

pythonProcess.stderr.on('data', (data) => {
const errorData = data.toString();
console.error(`错误输出: ${errorData}`);
event.reply('pyexecOutput', { status: 'error', output: errorData });
});

pythonProcess.on('close', () => {
// 发送剩余的数据
if (outputBuffer.length > 0) {
event.reply('pyexecOutput', { status: 'success', output: outputBuffer });
}
console.log('执行完毕');
});
});
image-20250303103724641

这样输出就会变得整齐,但是有一些连续数据可能会从中间劈开,比如以下图中,六位数字可能因为数据块大小原因从中间断开

因此在这种方法的基础上,可以定义Python每次输出的数据量大小,然后匹配main.js每次接收的数据块的大小然后再做其他处理

image-20250303103811062

例子3

这里再给出一个简单的Electron与Python通信的示例,实现一个计算功能

image-20250126185029689

首先在项目根目录下创建一个虚拟环境(名叫vvvv)

1
python -m venv vvvv

先写一段简单的Python代码,测试一下功能,从命令行输入一个包含加法算式的字符串,然后解析并计算其中所有数字的和,并输出结果。如果输入不合法或没有输入,程序会输出相应的提示信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from sys import argv
import re

def calculate_addition(expression):
numbers = re.findall(r'\d+', expression)
numbers = [int(num) for num in numbers]
return sum(numbers)

def calc(text):
try:
result = calculate_addition(text)
return result
except Exception as e:
print(e)
return 0

if __name__ == '__main__':
if len(argv) > 1:
print(calc(argv[1]))
else:
print("No input provided")

image-20250126185415485

渲染进程:index.html

1
2
3
4
5
6
7
<body>
<h1>Electron Python Calculator</h1>
<input id="expression" type="text" placeholder="Enter an expression (e.g., 1 + 2)">
<button id="calculate">Calculate</button>
<p>Result: <span id="result"></span></p>
<script src="renderer.js"></script>
</body>

renderer.js:

1
2
3
4
5
6
7
8
9
10
11
12
const expressInput = document.getElementById('expression');
const calculateButton = document.getElementById('calculate');
const resultSpan = document.getElementById('result');

calculateButton.addEventListener('click', () => {
const expression = expressInput.value;
electronAPI.calculateExpression(expression).then(result => {
resultSpan.textContent = result;
}).catch(error => {
resultSpan.textContent = error.message;
});
});

preload.js:

1
2
3
4
5
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('electronAPI', {
calculateExpression: (expression) => ipcRenderer.invoke('calculate-expression', expression)
});

main.js:

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
const { spawn } = require('child_process');

ipcMain.handle('calculate-expression', async (event, expression) => {
return new Promise((resolve, reject) => {
// 指定Python解释器的路径
const pythonPath = '.\\vvvv\\Scripts\\python.exe'; // 虚拟环境路径
// 拼接Python脚本的路径
const pythonScriptPath = path.join(__dirname, 'calc.py');
// 在子进程中启动Python解释器,传递表达式作为参数
const python = spawn(pythonPath, [pythonScriptPath, expression]);
let result = '';
// 监听Python子进程的标准输出
python.stdout.on('data', (data) => {
result += data.toString();
});
// 监听Python子进程的标准错误输出
python.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});
// 监听Python子进程的关闭事件
python.on('close', (code) => {
// 根据退出码判断脚本执行是否成功,并处理Promise
if (code === 0) {
resolve(result.trim());
} else {
reject(new Error(`Python script exited with code ${code}`));
}
});
});
});

789456123456

全局快捷键

全局快捷键只能在主进程中调用,所以直接不用考虑渲染进程

main.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.on('ready', () => {
let ret = globalShortcut.register('ctrl + q', () => {
// 按下快捷键的后续操作
})
if (!ret) {
console.log('register globalShortcut failed')
return
}
console.log(globalShortcut.isRegistered('ctrl + q'))
console.log(ret)
// 上面这两句打印的应该是一样的布尔值
})

// 注销快捷键
app.on('will-quit', () => {
globalShortcut.unregister('ctrl + q') // 注销某一个全局快捷键
globalShortcut.unregisterAll() // 注销所有快捷键
})

打包程序

根据[官方文档](打包您的应用程序 | Electron (electronjs.org)),使用Electron Forge

Electron Forge 是一个处理 Electron 应用程序打包与分发的一体化工具。 在工具底层,它将许多现有的 Electron 工具 (例如 @electron/packager@electron/osx-signelectron-winstaller 等) 组合到一起,因此您不必费心处理不同系统的打包工作。

这里不讲细节了,只说怎么打包

首先

1
2
npm install --save-dev @electron-forge/cli
npx electron-forge import

会有以下输出

image-20250126210142414

下面运行make

1
npm run make

报了以下错误

image-20250126210653162

网络错误,应该是因为挂了小猫,检查了一下,在系统环境变量里有一个这个:

image-20250126213349053

把上面这一条删掉,然后重启电脑(必须重启,不重启还不行)

再次npm run make

image-20250126213501397

这样就行了

但是这样打包完之后,打开安装包,程序会直接运行,然后直接安装到C盘,AppData/Local下面,很不方便,而且安装完之后的体积非常大,基本上啥功能没写,到了450MB,以后再找找怎么优化吧。。。




基础的基本上就这么多东西,开发Electron应用遵循这么一个步骤就行了:先前端开发(渲染进程),渲染进程做不了的(操作关于系统的东西,需要使用Node.js)使用preload.js预加载脚本交给main.js主进程来处理就好了,毕竟主进程非常强大的

恭喜你学会Electron的基本操作了,现在去用vscode开发一个vscode吧!