2022-无限咪咪-Electron 小记
Electron 小记
前言
第一次听到 Electron 这个技术大概是在 18、19 年的时候,当时觉得好神奇,以后 JSer 也可以写出客户端程序了。但是由于工作中用不到,个人项目中也没想到什么使用场景,所以跟着官方文档敲了遍 Demo 后也就不了了之了。
直到最近帮别人开发些小工具,才体会到这门技术的美。对于一些使用人数很少的项目,部署一套 Web 服务代价比较大,如果使用 Electron 开发,打包好程序后扔给对方一个压缩包就可以了,而且 Electron 还拥有与操作系统交互的能力,使得功能可以做的更强大。于是决定深入了解一下 Electron,以下是根据这套教程做的一些记录:Electron 教程。如有错漏之处,敬请指正。
首先要说明一点,这套视频教程的时间比较早,大概 17 年的样子。教程中使用的 Electron 版本是 1.6.6,写这篇文章的时候 Electron 大版本已经更新到 19 了。不过我认为问题不大,如果你不是功利性特别强,需要学完找工作的话,从 1 号大版本学习也未尝不好。主要学习的是思想,具体的 API 细节等理解了思想后,边查文档边开发效率也很高。
(咪咪看了下官方文档,从版本 1 到版本 19,对视频教程中有最大影响的就是版本 9 开始,无法在 renderer process 直接使用 “non-context-aware native modules”,这个是什么我没找到相关说明,反正像视频中在 Web 侧直接使用 require 方法是不行的。其余更新内容与视频的知识点关系不大,可放心食用。)
Electron 可以理解为就是 Google Chrome,前端代码运行在"浏览器"的 Tab 内,而 Electron 代码控制整个"浏览器"。
搭建环境
electron 依赖安装命令(安装 1 号大版本,跟视频中保持一致)
npm install --save-dev electron@1
不过由于某些原因,安装大概率会出错,可以使用这条命令替代
ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ npm install electron@1 -D --registry=https://registry.npm.taobao.org
这种包含 C++ 代码的库,安装起来都挺折腾的,初学者安装一个库花掉 1、2 个小时也不奇怪。
项目 1:获取视频时长
第一个项目是一个获取视频时长的程序。选择一个视频,然后在窗口中显示这个视频的时长(秒)。这个项目主要目的是熟悉 Electron 的基础。
最基础的 Electron 程序由两部分组成,App 与 BrowserWindow。App 代表整个应用,提供与应用有关的监听事件,属于 Electron 侧,而 BrowserWindow 可以创造与控制窗口,属于 Web 侧。
启动一个最简单的 Electron 程序
根目录新建 index.js 文件
// 引入依赖
const electron = require("electron");
const { app } = electron;
// app 监听 ready 事件
app.on("ready", () => {
console.log("app ready");
});
package.json 文件新增一行
{
...
"scripts": {
...
"start": "electron ."
},
...
}
然后在命令输入
npm run start
不出意外的话,控制台会输出
app ready
这时我们的 App 进程就已经启动了,进程会持续运行,Windows 用户可以通过 Ctrl + c 终止进程。
默认情况下,electron App process 不会向用户展示任何信息。
现在除了控制台的输出外,屏幕上不会出现窗口,因为窗口是 Web 侧的功能。
首先在根目录新增 index.html 文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>首页</title>
</head>
<body>
<h1>首页</h1>
</body>
</html>
在 index.js 中增加 BrowserWindow 相关代码
...
const { app, BrowserWindow } = electron;
// 主窗口作为全局变量
let mainWindow;
app.on("ready", () => {
// 新建 BrowserWindow 实例
mainWindow = new BrowserWindow({});
// 窗口加载 html 文件
// __dirname 代表当前模块的目录名
mainWindow.loadURL(`file://${__dirname}/index.html`);
});
启动命令
npm run start
等待一小会儿后,程序窗口就有了,展示的内容就是我们加载的 HTML 文件内容。
至此大概已经对 Electron 中的 App 与 BrowserWindow 是什么有点概念了吧。还记得我们这个程序要做什么吗,用户选择视频后,输出视频的时长。
先来说一下如何选择视频,在 index.html 中增加代码
<body>
<h1>首页</h1>
<div>
<form>
<label for="upload-btn">上传视频</label>
<input id="upload-btn" type="file" accept="video/*" />
<button type="submit">获取信息</button>
</form>
</div>
<script>
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
const file = document.querySelector("#upload-btn").files[0];
debugger;
const { path } = file;
});
</script>
</body>
然后启动程序
npm run start
打开调试器
通过程序菜单打开调试器
点击”上传视频“按钮并选择文件,然后点击”获取信息“按钮,程序会停在断点处,鼠标悬浮到 file 上可以看到视频的一些信息
上传视频的信息
这个上传是通过前端实现的,没有 Electron 的逻辑。可以看到虽然相比纯 Web 上传多了一些文件的信息(path、size),但是没有我们要的视频长度,这个功能视频教程中是靠 FFmpeg 实现的。
首先要安装 FFmpeg
Windows 用户参考 https://blog.csdn.net/qq_59636442/article/details/124526107
苹果用户使用 Homebrew 安装
安装完成后,在控制台输入
ffmpeg -version
输出相关信息表示安装成功了,然后我们要安装 fluent-ffmpeg 库,这个库是对 FFmpeg 的一层封装,方便 Node.js 使用 FFmpeg 的功能,使用 npm 安装
npm install fluent-ffmpeg
安装完成后,解析视频的准备工作就做好了。
视频教程里将解析视频长度的逻辑放在了 index.js 文件中,也就是 Electron 侧。视频中老版本 Electron 是可以在浏览器侧使用 Node.js Api 的,这时是靠约定来实现代码复用的。操作系统级别的逻辑,尽可能地放在 Electron 侧,让 Web 侧的逻辑保持简洁。
现在我们遇到问题了,刚刚上传的视频文件是在 Web 侧上传的,获取的文件信息也在 Web 侧,那如何将 Web 侧的信息传递给 Electron 侧呢?
进程间通信通过 ipc(Inter-Process Communication)来实现,Electron 中只要掌握下列四个方法就可以了
- Web 侧发送:ipcRenderer.send
- Electron 侧接收:ipcMain.on
- Electron 侧发送:mainWindow.webContents.send
- Web 侧接收:ipcRenderer.on
mainWindow 为 BrowserWindow 的实例,可以为任意名字。不同的窗口调用此方法,就是将信息发送到对应的窗口。
通过 ipc 将文件路径发送给 Electron 侧,index.html 中增加如下代码
...
<script>
// 引入依赖
const electron = require("electron");
const { ipcRenderer } = electron;
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
const { path } = document.querySelector("#upload-btn").files[0];
// Web 侧发送消息
ipcRenderer.send("video:submit", path);
});
</script>
...
在 script 标签中使用 require 方法,第一眼看到肯定会感到有点违和,这是 Electron 提供给我们的能力。(新版本无法使用这种方法,需要通过事先加载 preload.js ,使得 Web 侧获取调用 Node.js API 的能力)
Web 侧发送了视频路径,我们在 Electron 侧接收消息,index.js 中增加对应代码
const { app, BrowserWindow, ipcMain } = electron;
// 引入 ffmpeg 相关依赖
const ffmpeg = require("fluent-ffmpeg");
let mainWindow;
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/index.html`);
});
// Electron 侧接收消息
ipcMain.on("video:submit", (e, path) => {
console.log(path);
// ffmpeg.ffprobe 方法获取视频信息
ffmpeg.ffprobe(path, (err, metadata) => {
console.log(metadata.format.duration);
// 视频时长,单位秒
const d = metadata.format.duration;
// Electron 侧发送消息
mainWindow.webContents.send("video:duration", metadata.format.duration);
});
});
获取视频时长后,再发送给 Web 侧,Web 侧接收后在视图上展示
...
<script>
const electron = require("electron");
const { ipcRenderer } = electron;
// Web 侧接收消息
ipcRenderer.on("video:duration", (e, duration) => {
// 事件处理函数第二个参数就是发送的参数
console.log(duration);
document.querySelector("#video-duration").innerHTML = duration;
});
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
const { path } = document.querySelector("#upload-btn").files[0];
ipcRenderer.send("video:submit", path);
});
</script>
...
项目小结,通过这个项目,我们学习到
- 如何启动 Electron App
- 如何新建窗口
- Electron 中如何进程间通信,传递信息
- 如何使用 FFmpeg 获取视频信息
项目 2:TODO LIST
第二个项目是一个经典项目——待办列表。通过这个项目我们将学习到如何自定义下拉菜单以及多窗口。
首先新建一个项目,根据前面搭建环境的方法,搭建出一套 Electron 初始项目模板,还是以 index.js 作为程序入口,index.html 作为程序主窗口试图。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h1>TODO LIST</h1>
<ul></ul>
</body>
</html>
我们先增加一个菜单,菜单下拉后有“添加 TODO”和“退出”两个子菜单。自定义下拉菜单通过配置菜单模板实现,菜单模板格式如下
const menuTemplateItem = {
// 菜单名
label: "",
// 快捷键
accelerator: "",
// 点击事件回调
click() {},
// 下级菜单列表
submenu: [],
};
在 index.js 中定义菜单模板
// 菜单模板
const menuTemplate = [
{
// 第一层菜单固定在窗口上方
label: "文件",
// 文件的子菜单
submenu: [
{
label: "添加TODO",
},
{
label: "退出",
accelerator: (() => {
// 通过 process.platform 可以获取用户操作系统信息
// darwin 就是苹果系统
if (process.platform === "darwin") {
return "Command+Q";
} else {
return "Ctrl+Q";
}
})(),
click() {
// 点击退出程序
app.quit();
},
},
],
},
];
定义完菜单模板后,在 App ready 事件回调中设置菜单
const electron = require("electron");
const { app, BrowserWindow, Menu } = electron;
const menuTemplate = [...];
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/index.html`);
mainWindow.on("closed", () => {
app.quit();
});
// 通过 Menu.buildFromTemplate 方法将菜单模板转为 electron.Menu 对象
const menu = Menu.buildFromTemplate(menuTemplate);
// 设置应用菜单
Menu.setApplicationMenu(menu);
});
启动程序,可以看到我们自定义菜单了
自定义菜单
这时点击“添加 TODO”菜单是没有任何反应的,接下来我们增加新建窗口的逻辑。
首先新建 add.html 文件作为新增 TODO 窗口的视图
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<form>
<div>
<label>新增TODO</label>
<input type="text" autofocus />
</div>
<button type="submit">提交</button>
</form>
</body>
</html>
新建窗口的逻辑还是通过 BrowserWindow 类来实现的,但是这次我们把新建逻辑封装成一个函数,在 index.js 增加 createAddWindow 函数
// 同 mainWindow 一样,将新增窗口设为全局变量
let addWindow;
function createAddWindow() {
// 和新增主窗口的步骤一样,new BrowserWindow 的实例,然后加载 add.html 作为视图
addWindow = new BrowserWindow({
// 创建实例时,可以传参设定窗口属性
width: 300,
height: 200,
title: "新增TODO",
});
addWindow.loadURL(`file://${__dirname}/add.html`);
// 窗口关闭后,虽然视图消失了,但是 addWindow 指向的对象还在内存中
// 通过手动赋值 null 释放之前的对象
addWindow.on("closed", () => (addWindow = null));
}
菜单模板增加“添加 TODO”菜单的点击事件
// ...
const menuTemplate = [
{
label: "文件",
submenu: [
{
label: "添加TODO",
click() {
// 调用创建新增窗口函数
createAddWindow();
},
},
{
label: "退出",
accelerator: (() => {
if (process.platform === "darwin") {
return "Command+Q";
} else {
return "Ctrl+Q";
}
})(),
click() {
app.quit();
},
},
],
},
];
// ...
重新启动程序,然后点击菜单“文件”-“添加 TODO”,此时会弹出新建 TODO 窗口了
新建 TODO 窗口
最后就是实现新增 TODO,然后展示在主窗口的功能了。实现这个功能还是要使用到我们之前提到的 ipc 的四个方法。数据流向:新增窗口(add.html) ==》index.js ==》主窗口(index.html)。
在 add.html 中增加发送事件,将输入的新 TODO 的值发送给 index.js
<body>
<!-- ... -->
<script>
const electron = require("electron");
const { ipcRenderer } = electron;
document.querySelector("form").addEventListener("submit", (e) => {
e.preventDefault();
// Web 侧发送消息
// 将新增TODO的值发送给 index.js
ipcRenderer.send("todo:add", document.querySelector("input").value);
});
</script>
<!-- ... -->
</body>
index.js 中接收新 TODO 的值,并发送到 index.html 中
const electron = require("electron");
const { app, BrowserWindow, Menu, ipcMain } = electron;
let mainWindow;
let addWindow;
// ...
// Electron 侧接收消息
ipcMain.on("todo:add", (e, newTodo) => {
// Electron 侧发送消息
// 发送给 index.html 那个窗口
mainWindow.webContents.send("todo:add", newTodo);
// 程序一般设计就是新增后,自动关闭新增窗口
addWindow.close();
});
在 index.html 中接收 Electron 侧发来的消息
<body>
<h1>TODO LIST</h1>
<ul></ul>
<script>
const electron = require("electron");
const { ipcRenderer } = electron;
const ul = document.querySelector("ul");
// Web 侧接收消息
ipcRenderer.on("todo:add", (e, newTodo) => {
console.log(newTodo);
// 将新 TODO 的值显示到页面上
const li = document.createElement("li");
const todo = document.createTextNode(newTodo);
li.appendChild(todo);
ul.appendChild(li);
});
</script>
</body>
重新启动程序,试试看新建 TODO 吧
新建 TODO 成功!
至此我们项目二的主要功能就实现了,了解了自定义菜单与窗口间通信。但是还有一个问题没有解决,细心的童鞋可能发现了,我们自定义菜单后,原来默认的菜单就不见了。
Electron 程序默认的菜单
更要命的是,这些菜单不但在窗口中不显示了,它们对应的快捷键也失效了。比如现在我们就无法打开调试页面了,这使得在开发时就很不方便。
如何恢复调试功能呢,其实很简单,在 index.js 中添加以下逻辑
// process.env.NODE_ENV 来获取 Node.Js 的环境变量,一般开发环境的值设置为 "production"
// 调试功能我们不希望用户在生产环境中使用,所以只有非生产环境,才添加对应的菜单
if (process.env.NODE_ENV !== "production") {
// 菜单模板增加"开发"菜单
// 子菜单为 "打开调试器"
menuTemplate.push({
label: "开发",
submenu: [
{
label: "打开调试器",
click(item, focusedWindow) {
// 点击回调函数的第二个参数是当前 focus 窗口的引用
// 通过 .toggleDevTools 方法打开调试器
focusedWindow.toggleDevTools();
},
},
],
});
}
现在重新运行程序,菜单栏多了“开发”,这样我们就可以开发坏境下打开调试器了。
除了调试器,我们还希望增加刷新页面功能,Web 侧的改动可以直接刷新更新,只需要在刚刚的代码中做一点小小的改动
if (process.env.NODE_ENV !== "production") {
menuTemplate.push({
label: "开发",
submenu: [
// role 也是菜单模板对象中的属性,通过这个属性可以使用一些预设好的功能
// "reload"就是刷新,还有其他值可以参考官网文档
{ role: "reload" },
{
label: "打开调试器",
click(item, focusedWindow) {
focusedWindow.toggleDevTools();
},
},
],
});
}
再重新启动程序,现在“开发”菜单中不但增加了“Reload”子菜单,而且还增加了快捷键刷新。
项目小结,通过这个项目,我们学习到
- 如何自定义菜单
- 如何进行窗口间通信
项目 3:时钟
到第三个项目了,经过前两个项目的学习,我们现在对 Electron 的主要功能有了一定的了解了,接下来通过这个项目,我们学习一下 Electron 程序如何与状态栏(Tray)交互。
这是个时间管理项目,在状态栏点击程序 icon 后,弹出窗口进行时间操作。
这个项目要先去下载作者的模板代码,下载地址
https://github.com/StephenGrider/ElectronCode
下载或克隆下来的项目,进入 \boilerplates\tasky 文件夹,然后在 package.json 中,把 electron 的版本改成 1.8.8(原版可能会有安装问题),版本号改好后,在命令行执行 npm 安装命令
ELECTRON_MIRROR=http://npm.taobao.org/mirrors/electron/ npm install --registry=https://registry.npm.taobao.org
等待依赖安装完毕,在项目根目录新建 index.js 文件,增加 Electron “模板”代码,注意 main 窗口加载的 HTML 文件路径
const electron = require("electron");
const { app, BrowserWindow } = electron;
let mainWindow;
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/src/index.html`);
});
这个项目和我们之前两个项目有一点点区别,这个项目的前端是用 React 写的,如果你不熟悉 React 也没有关系,我们的重点还是放在 Electron 上,只是如何运行这个项目需要注意下,需要先启动前端项目,在命令行输入
npm run start
等待几秒后,在命令窗口看到下面的输出说明前端项目启动好了
Version: webpack 2.7.0
Time: 2942ms
Asset Size Chunks Chunk Names
bundle.js 231 kB 0 [emitted] main
chunk {0} bundle.js (main) 227 kB [entry] [rendered]
[4] ./src/index.js 721 bytes {0} [built]
[5] (webpack)-dev-server/client?http://localhost:4172 7.93 kB {0} [built]
[6] ./src/components/App.js 9.73 kB {0} [built]
[7] ./src/components/Header.js 1.35 kB {0} [built]
[8] ./src/components/Settings.js 5.62 kB {0} [built]
[9] ./src/components/TasksIndex.js 4.42 kB {0} [built]
[10] ./src/components/TasksShow.js 6.05 kB {0} [built]
[11] ./src/utils/Timer.js 4.48 kB {0} [built]
[12] ./~/sockjs-client/dist/sockjs.js 181 kB {0} [built]
[13] (webpack)-dev-server/client/overlay.js 3.67 kB {0} [built]
[14] (webpack)-dev-server/client/socket.js 1.08 kB {0} [built]
[15] (webpack)/hot nonrecursive ^\.\/log$ 160 bytes {0} [built]
[16] (webpack)/hot/emitter.js 77 bytes {0} [built]
[25] multi (webpack)-dev-server/client?http://localhost:4172 ./src/index.js 40 bytes {0} [built]
+ 12 hidden modules
webpack: Compiled successfully.
此时在新的命令窗口,运行我们 Electron 项目
npm run electron
运行成功后,应该会看到以下窗口
项目 3 初始
走到这里我们项目 3 的准备工作就做好了,接下来我们要实现程序在状态栏的展示。
Electron 使用 Tray 类来实现程序在状态栏的逻辑,在 index.js 增加代码
// 引入 path 模块
const path = require("path");
const electron = require("electron");
const { app, BrowserWindow, Tray } = electron;
let mainWindow;
// 全局变量保存 Tray 实例
let tray;
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/src/index.html`);
// 我这里偷懒了,视频中通过 process.platform 来判断当前操作系统是否是 windows 系统
// windows 系统使用 windows-icon@2x.png, 否则使用 iconTemplate.png
const iconPath = path.join(__dirname, "./src/assets/windows-icon@2x.png");
// 新建 Tray 实例
tray = new Tray(iconPath);
// 增加状态栏 icon 的悬浮提示
tray.setToolTip("提示");
});
重新启动项目(只要将当前 Electron 的进程杀死就行,前端的进程不需要重启),运行完毕后,状态栏就有了图标展示了,鼠标悬浮 icon 还会有文字提示
状态栏 icon
视频中这时没有将 tray 作为全局变量保存,咪咪我在 windows10 的机器上运行程序,icon 会出现闪退,原因就是局部变量的 Tray 实例被内存垃圾回收导致的,所以必须设置成全局变量才行。
接下来我们添加状态栏 icon 的点击事件,在 index.js 增加 tray 的监听事件
// ...
app.on("ready", () => {
mainWindow = new BrowserWindow({});
mainWindow.loadURL(`file://${__dirname}/src/index.html`);
const iconPath = path.join(__dirname, "./src/assets/windows-icon@2x.png");
tray = new Tray(iconPath);
tray.setToolTip("提示");
// Tray 实例增加 click 事件的监听
tray.on("click", (e) => {
// 通过 window.isVisible() 获取窗口是否显示
if (mainWindow.isVisible()) {
// 如果窗口已经显示就隐藏窗口
mainWindow.hide();
} else {
// 如果窗口隐藏状态就显示
mainWindow.show();
}
});
});
重启项目,此时点击 icon 已经实现了类似 toggle 窗口的效果。
如果作为普通程序,那做到这一步就差不多了,但是这次我们想实现一个类似小工具的程序,点击状态栏的 icon 后,直接在附近弹出一个小窗口,在窗口中进行操作,而不是把 icon 作为开关程序的入口而已。
要实现这个效果,关键有两件事,第一件需要重设窗口的外观,使其更像“小工具”的弹窗,第二件就是定位窗口的位置,使其出现在状态栏附近,在 index.js 中添加代码
// ...
app.on("ready", () => {
// 设置主窗口的初始化参数
mainWindow = new BrowserWindow({
// 窗口宽度
width: 300,
// 窗口高度
height: 500,
// 不展示边框(没有菜单栏)
frame: false,
// 用户不能重新更改窗口大小
resizable: false,
// 默认运行时不展示窗口
show: false,
// 在任务栏不展示程序
skipTaskbar: true,
webPreferences: {
// 窗口不显示时,后台是否继续运行计时器和动画,默认是 true 不运行
backgroundThrottling: false,
},
});
mainWindow.loadURL(`file://${__dirname}/src/index.html`);
// 增加窗口 blur 事件,窗口一旦失去聚焦,自动隐藏窗口
mainWindow.on("blur", () => {
mainWindow.hide();
});
const iconPath = path.join(__dirname, "./src/assets/windows-icon@2x.png");
tray = new Tray(iconPath);
tray.setToolTip("提示");
// icon 的点击回调的第二个参数是 icon 的坐标
tray.on("click", (e, bound) => {
// x - icon 左上角的 x 坐标
// y - icon 左上角的 y 坐标
const { x, y } = bound;
// 通过 window.getBounds 方法动态获取窗口的宽高
const { width, height } = mainWindow.getBounds();
if (mainWindow.isVisible()) {
mainWindow.hide();
} else {
// 设置窗口的信息,注意这是 windows 下,任务栏在窗口下方的计算方法
mainWindow.setBounds({
// 窗口左上角的 x 坐标,要比 icon 左上角的 x 坐标,"往左"一半窗口的宽度
x: x - width / 2,
// 窗口左上角 y 坐标,要比 icon 左上角 y 坐标 "往上"窗口的高度
y: y - height,
// 窗口的宽高保持不变
width,
height,
});
mainWindow.show();
}
});
});
(视频中隐藏程序在任务栏的显示是通过 App.dock.hide 实现的,这个方法只有苹果系统中才可以用,Windows 系统下 App.dock 是 undefined,如果要隐藏任务栏的显示要通过 BrowserWindow 新建实例时传参 skipTaskbar)
Mac OS X 是面向应用程序的,而 Windows 是面向窗口的。
重新运行程序,现在点击状态栏的 icon,会在上方弹出窗口(如果是苹果系统,窗口坐标的计算方式与 Windows 不同,视频中的老师是苹果系统),而且此时窗口的边框也没有了。
窗口直接定位在 icon 上方
屏幕的左上角坐标为(0, 0),越往右边 x 越大,越往下边 y 越大。屏幕可视范围内,坐标的 x、y 值应该都是正数。窗口、icon 的坐标,都是以自己左上角的那个像素为标准。
(视频中的代码其实有一点点问题,窗口的 blur 事件和 icon 的点击事件会有冲突,在窗口显示时,点击 icon 会优先触发窗口的 blur 事件导致窗口隐藏,icon 的点击事件又会导致窗口重新显示,使得原本的 toggle 效果失效)
最后我们为 icon 增加右键菜单功能,Tray 添加菜单同 Menu 一样,也是通过菜单模板实现的,具体代码如下
// ...
const { app, BrowserWindow, Tray, Menu } = electron;
// ...
// icon 的右键点击菜单模板,模板格式同自定义菜单一样
const menuTemplate = Menu.buildFromTemplate([
{
label: "退出",
click() {
app.quit();
},
},
]);
app.on("ready", () => {
// ...
// tray 增加鼠标右键点击监听
tray.on("right-click", () => {
// tray 通过 .popUpContextMenu 方法弹出菜单
tray.popUpContextMenu(menuTemplate);
});
});
重启程序,现在右键点击 icon 有菜单弹出了。
至此项目 3 就进行的差不多了,视频中还有两点文章中没有说明,一个是视频中老师将 Tray 等功能封装成单独的 Class 了,这属于 ES6 特性,感兴趣的同学可以自己尝试下。还有一点就是视频中通过 tray.setTitle 方法设置 icon 区域的文字展示,这个应该是苹果系统下独有的功能,Windows 用户了解一下即可,由于方法本身也比较简单,这里就不多赘述了。
项目 3 小结
- 如何在状态栏实现 icon 相关逻辑
- 窗口的属性设置
项目 4:视频格式转换
最后一个项目,是将输入的视频批量转换格式。这个项目相当于是对前两个 Electron 项目知识点的一个总结。
对于比较扎实的掌握了前面三个项目的话,这个项目可以不看。转换视频格式核心是通过 FFmpeg 实现的,进程间通信使用项目 1 中提到的 ipc 那四个方法,最后通过 shell 模块的 .showItemInFolder 方法打开转换后文件夹即可。