Electron 桌面应用开发
构建 使用npm构建经常报错,使用以下方法构建
1.打开npm配置文件
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
构建成功
运行原理 下面通过源码阅读的方式简要梳理Electron应用程序的原理
首先git clone https://github.com/electron/electron-quick-start
,下载一个demo示例
使用npm start
,就可以运行这个Electron应用程序
首先看前两行
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
我们在第一次运行这个程序的时候看到有一些数值,比如版本之类,但是前端是没有写出来的,这些前端中的数据是可以通过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
生命周期
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" ) }) });
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
:距离屏幕左上角纵向距离
width
、height
:窗口的宽度、高度
maxWidth
、maxHeight
、minWidth
、minHeight
:窗口的最大、最小宽度、高度
show
:true
/false
,窗口创建后是否直接显示
resizable
:true
/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' ) }) })
创建新窗口 这里采取一个点击一个按钮,弹出一个新窗口的方式实现
首先,优化一下目录结构,采用与微信小程序相同的目录结构,方便管理
首先在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出于安全性的考虑
所以这就需要我们的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,则会让整个应用中没有元素的地方都变成透明的(但是鼠标点击穿透不了)
但是此时如果你添加了一个按钮用来最大化窗口(maximize
或unmaximize
)你会发现你能够最大化(最大化窗口)但是没法恢复(窗口模式),这貌似是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 let xx = 0 let yy = 0 function isPointInBounds (x, y, bounds ) { 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 )) { xx = bounds.x ; yy = bounds.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 ) => { console .log (mainWindow.isMaximized ()) if (!ism) { ism = true mainWindow.maximize () mainWindow.resizable = false mainWindow.movable = false event.reply ('receive-maximize-message' , true ) } else { ism = false mainWindow.close () mainWindow = cw () mainWindow.loadFile ('pages/index/index.html' ) mainWindow.once ('ready-to-show' , () => { mainWindow.show () }) mainWindow.on ('maximize' , () => { checkWindowScreen (mainWindow) }) 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加载不出来就等会 ~):
学习这部分之前请先了解下面的 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 , 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的主要部分,非客户区是我自己定义的,比较长一坨
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 > <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 > <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 () } }) document .getElementById ('IgnoreBackground' ).addEventListener ('mouseleave' , (event ) => { if (event.target .id === 'IgnoreBackground' ) { window .CustomAPI .unIgnoreM () } })
现在基本功能完成了,但是鼠标移入到客户区 (id=”IgnoreBackground”) 的时候,里面的子元素没法点击啊,鼠标仍然被判断为没有移出客户区
这时候就给客户区中的所有下一级子元素绑定一个class(也就是上面html中的class=”chd”),然后给所有有这个class的子元素监听mouseover
和mouseleave
,然后阻止子元素事件冒泡到父元素
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: 不会区分mouseenter
、mouseleave
、mouseover
、mouseout
四者的区别先自行百度 bing一下
设置完这一堆之后,就能在透明的地方实现鼠标穿透点击等操作,在绑定了class=”chd”的地方仍然能对Electron程序中的元素进行操作
主题切换 这个东西比较简单,只把主要代码放在这里了,可以抄一下(以下是完整代码,所以比较长)
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 > <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% ; 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 window .addEventListener ('DOMContentLoaded' , () => { document .getElementById ('minisize' ).addEventListener ('click' , (event ) => { event.stopPropagation () window .FunctionBar .minimize () }) document .getElementById ('maxsize' ).addEventListener ('click' , (event ) => { event.stopPropagation () window .FunctionBar .maximize () }) document .getElementById ('close' ).addEventListener ('click' , (event ) => { event.stopPropagation () window .FunctionBar .close () }) 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)如下:
渲染进程向主进程通信
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
相同,主进程即可向渲染进场回复
主进程主动向渲染进程通信 比如我在实现窗口maximize
和unmaximize
的时候,我不使用系统默认的最大化图标,而是完全自定义,那么我如果不点击按钮进行放大,而是双击程序的标题栏,或者拖动到屏幕顶端进行最大化,那么就需要主进程检测是否最大化了,然后通知渲染进程改变最大化的图标 →
main.js监听程序是否maximized
或unmaximized
,并分别发送到window-maximized
和window-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 () }); window .abcMaximize .onMessage ('window-unmaximized' , (arg ) => { setMinimizeIcon () });
以上三个例子说明了使用contextBridge方法进行ipc通信的示例,还有其他的方法,这里就不讲了,因为简直易(wo)如(bu)反(xiang)掌(xue)
消息通知 像Windows那样,在桌面右下角弹出一个通知窗口,点击后有相应的操作
官方文档说在渲染进程中可以直接操作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) });
点击后右下角就会弹出这个东西
怎么办怎么办?! 就在此时,你小手一抖!不小心点上了关闭electron.app.Electron的所有通知! 你火急火燎的打开了设置>系统>通知,从上到下翻了好几遍,发现没有能打开它的地方?!?!?!
打开C:\Users\<username>\AppData\Local\Microsoft\Windows\Notifications
这个目录,里面有一个wpndatabase.db
数据库,打开它,里面长这样:
先打开NotificationHandler
表,拉到最底下,找到electron的应用,因该是一找就能找到,记住前面的序号,543
打开另一张表HandlerSettings
,里面长这样:
点击第一列HandlerId
进行倒序,找到刚才对应的 ID 543
找到这个s:toast
,如果你关闭了通知,那么这里应该是0,把它改成1
然后打开注册表,去到这个路径:计算机\HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Notifications\Settings\
,找到Electron的程序名,里面应该会发现多了一个Enable
为0
的项,直接把这个项删掉
然后重启电脑,消失的通知就又回来了!
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 ) })
以下结果:
例子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>' })
但是由于执行速度和获取速度问题,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 ; pythonProcess.stdout .on ('data' , (data ) => { outputBuffer += data.toString (); 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 ('执行完毕' ); }); });
这样输出就会变得整齐,但是有一些连续数据可能会从中间劈开,比如以下图中,六位数字可能因为数据块大小原因从中间断开
因此在这种方法的基础上,可以定义Python每次输出的数据量大小,然后匹配main.js每次接收的数据块的大小然后再做其他处理
例子3 这里再给出一个简单的Electron与Python通信的示例,实现一个计算功能
首先在项目根目录下创建一个虚拟环境(名叫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 argvimport redef 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" )
渲染进程: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 ) => { const pythonPath = '.\\vvvv\\Scripts\\python.exe' ; const pythonScriptPath = path.join (__dirname, 'calc.py' ); const python = spawn (pythonPath, [pythonScriptPath, expression]); let result = '' ; python.stdout .on ('data' , (data ) => { result += data.toString (); }); python.stderr .on ('data' , (data ) => { console .error (`stderr: ${data} ` ); }); python.on ('close' , (code ) => { if (code === 0 ) { resolve (result.trim ()); } else { reject (new Error (`Python script exited with code ${code} ` )); } }); }); });
全局快捷键 全局快捷键只能在主进程中调用,所以直接不用考虑渲染进程
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-sign
、electron-winstaller
等) 组合到一起,因此您不必费心处理不同系统的打包工作。
这里不讲细节了,只说怎么打包
首先
1 2 npm install --save-dev @electron-forge/cli npx electron-forge import
会有以下输出
下面运行make
报了以下错误
网络错误,应该是因为挂了小猫,检查了一下,在系统环境变量里有一个这个:
把上面这一条删掉,然后重启电脑(必须重启,不重启还不行)
再次npm run make
这样就行了
但是这样打包完之后,打开安装包,程序会直接运行,然后直接安装到C盘,AppData/Local下面,很不方便,而且安装完之后的体积非常大,基本上啥功能没写,到了450MB,以后再找找怎么优化吧。。。
基础的基本上就这么多东西,开发Electron应用遵循这么一个步骤就行了:先前端开发(渲染进程),渲染进程做不了的(操作关于系统的东西,需要使用Node.js)使用preload.js预加载脚本交给main.js主进程来处理就好了,毕竟主进程非常强大的
恭喜你学会Electron的基本操作了,现在去用vscode开发一个vscode吧!