应用

Dialog与文件选择

显示系统对话框, 提供了消息提示、消息提示操作以及选择文件、保存文件等操作.

消息提示 dialog.showMessageBoxSync

消息提示

1
2
3
4
5
6
7
8
9
// @@code-renderer: runner
// @@code-props: { height: '130px' }
const { dialog } = require('electron')
dialog.showMessageBoxSync({
type: 'info',
title: '这里是标题',
message: '提示内容',
detail: '额外信息'
})

消息提示与确认

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// @@code-renderer: runner
// @@code-props: { height: '240px' }
const { dialog } = require('electron')
const res = dialog.showMessageBoxSync({
type: 'info',
title: '这里是标题',
message: '提示内容',
detail: '额外信息',
cancelId: 1, // 按esc默认点击索引按钮
defaultId: 0, // 默认高亮的按钮下标
buttons: ['确认按钮', '取消按钮'], // 按钮按索引从右往左排序
})
console.log('操作结果', res, res === 0 ? '点击确认按钮' : '点击取消按钮') // 根据按钮数组中的下标来判断
console.log('操作中还有个checkboxLabel的单选框需要使用showMessageBox api才可以获取到返回值')

API说明

dialog.showMessageBoxSync(browserWindow, options)

显示一个消息框,它将阻止进程,直到消息框被关闭。返回值为点击的按钮的索引。

参数:

  • browserWindow

    可以指定一个父窗口,作为模态窗口附加到该窗口。

  • options

    • type: String (可选) - “none” | “info” | “error” | “question” 不同的type提示的图标不同;

    • title: String (可选) - message box 的标题,一些平台不显示,建议使用message和detail;

    • message: String - message box 的内容.

    • detail: String (可选) - 额外信息

    • buttons String[] - 字符串按钮数组,按钮按索引从右往左排序,如果未指定默认有一个”OK”的按钮。

    • defaultId: Integer (可选) - 默认高亮的按钮下标,回车的时候自动选中该项

    • cancelId: Integer (可选) 按esc默认点击索引按钮

返回值类型:

  • number: 所点击的按钮的索引

dialog.showMessageBox(browserWindow, options)

与dialog.showMessageBoxSync类似,不同点在于:

  1. 这是一个异步方法,返回值为Promise类型;
  2. 显示的对话框可以指定一个复选框,返回值中也增加了对应的字段;

下面是带复选框的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// @@code-renderer: runner
// @@code-props: { height: '280px' }
const { dialog } = require('electron')
const res = dialog.showMessageBox({
type: 'info',
title: '这里是标题',
message: '提示内容',
detail: '额外信息',
cancelId: 1, // 按esc默认点击索引按钮
defaultId: 0, // 默认高亮的按钮下标
checkboxLabel: '单选框内容',
checkboxChecked: false, // 是否选中单选框
buttons: ['确认按钮', '取消按钮'], // 按钮按索引从右往左排序
})
console.log('操作结果 promise', res) // 返回一个promise可以通过它判断结果

选择文件和文件夹

选择文件实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// @@code-renderer: runner
// @@code-props: { height: '320px' }
const { dialog, app } = require('electron')
const res = dialog.showOpenDialogSync({
title: '对话框窗口的标题',
// 默认打开的路径,比如这里默认打开下载文件夹
defaultPath: app.getPath('downloads'),
buttonLabel: '确认按钮文案',
// 限制能够选择的文件类型
filters: [
// { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
// { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] },
// { name: 'Custom File Type', extensions: ['as'] },
// { name: 'All Files', extensions: ['*'] },
],
properties: [ 'openFile', 'openDirectory', 'multiSelections', 'showHiddenFiles' ],
message: 'mac文件选择器title'
})
console.log('res', res)

API说明

dialog.showOpenDialogSync(browserWindow,options)

参数:

options

  • defaultPath String (可选) - 设置对话框默认打开哪个路径,需要设置一个有效路径否则将不生效。

  • buttonLabel String (可选) - 确认按钮的文案, 当为空时, 将使用默认标签

  • filters 默认所有文件类型都可以选择,设置后,只能选择允许的文件类型

  • properties String[] (可选)

    • openFile - 允许选择文件
    • openDirectory - 允许选择文件夹
    • multiSelections - 允许多选。
    • showHiddenFiles - 显示对话框中的隐藏文件
  • message String (可选) - mac文件选择器的title

tips: 尝试修改options中的参数来查看效果;

返回值类型:

String[] | undefined - 用户选择的文件或文件夹路径;如果取消对话框,则返回undefined

完整API解释参考文档

保存文件

实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// @@code-renderer: runner
// @@code-props: { height: '300px' }
const { dialog } = require('electron')
const res = dialog.showSaveDialogSync({
title: '对话框窗口的标题',
defaultPath: '', // 打开文件选择器的哪个路径 需要输入一个有效路径
buttonLabel: '确认按钮文案',
// 限制能够选择的文件为某些类型
filters: [
// { name: 'Images', extensions: ['jpg', 'png', 'gif'] },
// { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] },
// { name: 'Custom File Type', extensions: ['as'] },
// { name: 'All Files', extensions: ['*'] },
],
nameFieldLabel: '替换文件', // “文件名”文本字段前面显示的文本自定义标签
showsTagField: true, // 显示标签输入框,默认值为true
properties: [ 'showHiddenFiles' ],
message: 'mac文件选择器title'
})
console.log('res', res)

API说明

dialog.showSaveDialogSync(browserWindow,options)

参数:

options

  • defaultPath String (可选) - 设置对话框默认打开哪个路径,需要设置一个有效路径否则将不生效。

  • buttonLabel String (可选) - 确认按钮的文案, 当为空时, 将使用默认标签

  • filters 默认所有文件类型都可以选择,设置后,只能选择允许的文件类型

  • properties String[] (可选)

    • openFile - 允许选择文件
    • openDirectory - 允许选择文件夹
    • multiSelections - 允许多选。
    • showHiddenFiles - 显示对话框中的隐藏文件
  • message String (可选) - mac文件选择器的title

返回值类型:

String[] | undefined - 用户选择的文件或文件夹路径;如果取消对话框,则返回undefined;

完整API解释参考文档

不同场景表现

  1. 选择了一个存在的文件

    提示”文件夹中已有相同名称的文件或文件夹。替换它将覆盖其当前内容。“,点击确认后返回该文件地址

  2. 选择了一个不存在的文件

    返回该不存在的文件地址

错误信息弹窗

下载管理器

下载管理器

点击“试一试”按钮体验一下。

1
2
3
4
// @@code-renderer: runner
// @@code-props: {hideRight: true, mProcess: false}
const {ipcRenderer} = require('electron')
ipcRenderer.invoke('openDownloadManager', '/download-manager/demo')

文件下载是我们开发中比较常见的业务需求,比如:导出 excel。

web 应用文件下载存在一些局限性,通常是让后端将响应的头信息改成 Content-Disposition: attachment; filename=xxx.pdf,触发浏览器的下载行为。

在 electron 中的下载行为,都会触发 session 的 will-download 事件。在该事件里面可以获取到 downloadItem 对象,通过 downloadItem 对象实现一个简单的文件下载管理器:

效果图

如何触发下载

由于 electron 是基于 chromium 实现的,通过调用 webContents 的 downloadURL 方法,相当于调用了 chromium 底层实现的下载,会忽略响应头信息,触发 will-download 事件。

1
2
3
4
5
// 触发下载
win.webContents.downloadURL(url)

// 监听 will-download
session.defaultSession.on('will-download', (event, item, webContents) => {})

下载流程图

流程图

功能设计

在上面的效果图中,实现的简单文件下载管理器功能包含:

  • 设置保存路径
  • 暂停/恢复和取消
  • 下载进度
  • 下载速度
  • 下载完成
  • 打开文件和打开文件所在位置
  • 文件图标
  • 下载记录

设置保存路径

如果没有设置保存路径,electron 会自动弹出系统的保存对话框。不想使用系统的保存对话框,可以使用 setSavePath 方法,当有重名文件时,会直接覆盖下载。

1
item.setSavePath(path)

为了更好的用户体验,可以让用户自己选择保存位置操作。当点击位置输入框时,渲染进程通过 ipc 与主进程通信,打开系统文件选择对话框。

选择保存位置

主进程实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 打开文件选择框
* @param oldPath - 上一次打开的路径
*/
const openFileDialog = async (oldPath: string = app.getPath('downloads')) => {
if (!win) return oldPath

const { canceled, filePaths } = await dialog.showOpenDialog(win, {
title: '选择保存位置',
properties: ['openDirectory', 'createDirectory'],
defaultPath: oldPath,
})

return !canceled ? filePaths[0] : oldPath
}

ipcMain.handle('openFileDialog', (event, oldPath?: string) => openFileDialog(oldPath))

渲染进程代码:

1
2
3
const path = await ipcRenderer.invoke('openFileDialog', 'PATH')

console.log(path)

暂停/恢复和取消

拿到 downloadItem 后,暂停、恢复和取消分别调用 pauseresumecancel 方法。当我们要删除列表中正在下载的项,需要先调用 cancel 方法取消下载。

下载进度

downloadItem 中监听 updated 事件,可以实时获取到已下载的字节数据,来计算下载进度和每秒下载的速度。

1
2
// 计算下载进度
const progress = item.getReceivedBytes() / item.getTotalBytes()

下载进度

在下载的时候,想在 Mac 系统的程序坞和 Windows 系统的任务栏展示下载信息,比如:

  • 下载数:通过 app 的 badgeCount 属性设置,当为 0 时,不会显示。也可以通过 dock 的 setBadge 方法设置,该方法支持的是字符串,如果不要显示,需要设置为 ‘’。
  • 下载进度:通过窗口的 setProgressBar 方法设置。

由于 Mac 和 Windows 系统差异,下载数仅在 Mac 系统中生效。加上 process.platform === ‘darwin’ 条件,避免在非 Mac、Linux 系统下出现异常错误。

下载进度(Windows 系统任务栏、Mac 系统程序坞)显示效果:

windows 进度

mac 程序坞

1
2
3
4
5
6
7
8
// mac 程序坞显示下载数:
// 方式一
app.badgeCount = 1
// 方式二
app.dock.setBadge('1')

// mac 程序坞、windows 任务栏显示进度
win.setProgressBar(progress)

下载速度

由于 downloadItem 没有直接为我们提供方法或属性获取下载速度,需要自己实现。

思路:在 updated 事件里通过 getReceivedBytes 方法拿到本次下载的字节数据减去上一次下载的字节数据。

1
2
3
4
5
6
7
8
9
// 记录上一次下载的字节数据
let prevReceivedBytes = 0

item.on('updated', (e, state) => {
const receivedBytes = item.getReceivedBytes()
// 计算每秒下载的速度
downloadItem.speed = receivedBytes - prevReceivedBytes
prevReceivedBytes = receivedBytes
})

需要注意的是,updated 事件执行的时间约 500ms 一次。

updated_event

下载完成

当一个文件下载完成、中断或者被取消,需要通知渲染进程修改状态,通过监听 downloadItem 的 done 事件。

1
2
3
4
5
6
7
8
item.on('done', (e, state) => {
downloadItem.state = state
downloadItem.receivedBytes = item.getReceivedBytes()
downloadItem.lastModifiedTime = item.getLastModifiedTime()

// 通知渲染进程,更新下载状态
webContents.send('downloadItemDone', downloadItem)
})

打开文件和打开文件所在位置

使用 electron 的 shell 模块来实现打开文件(openPath)和打开文件所在位置(showItemInFolder)。

由于 openPath 方法支持返回值 Promise<string>,当不支持打开的文件,系统会有相应的提示,而 showItemInFolder 方法返回值是 void。如果需要更好的用户体验,可使用 nodejs 的 fs 模块,先检查文件是否存在。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import fs from 'fs'

// 打开文件
const openFile = (path: string): boolean => {
if (!fs.existsSync(path)) return false

shell.openPath(path)
return true
}

// 打开文件所在位置
const openFileInFolder = (path: string): boolean => {
if (!fs.existsSync(path)) return false

shell.showItemInFolder(path)
return true
}

文件图标

很方便的是使用 app 模块的 getFileIcon 方法来获取系统关联的文件图标,返回的是 Promise<NativeImage> 类型,我们可以用 toDataURL 方法转换成 base64,不需要我们去处理不同文件类型显示不同的图标。

1
2
3
4
5
6
7
8
9
10
const getFileIcon = async (path: string) => {
const iconDefault = './icon_default.png'
if (!path) Promise.resolve(iconDefault)

const icon = await app.getFileIcon(path, {
size: 'normal'
})

return icon.toDataURL()
}

下载记录

随着下载的历史数据越来越多,使用 electron-store 将下载记录保存在本地。

协议

协议自动唤起应用与自定义协议

协议: 从网页端唤起Electron应用

协议唤起示例:

什么是协议

electron注册的协议, electron会将协议注册到系统的协议列表中,它是系统层级的API,只能在当前系统下使用, 其他未注册协议的电脑不能识别。

Electron的app模块提供了一些处理协议的方法, 这些方法允许您设置协议和取消协议, 来让你的应用成为默认的应用程序。

协议的作用

注册一个协议到系统协议中, 当通过其他应用/浏览器网页端**打开新协议的链接时,浏览器会检测该协议有没有在系统协议中, 如果该协议注册过,然后唤起协议的默认处理程序(我们的应用)**。

注册协议: app.setAsDefaultProtocolClient

协议需要在ready事件后注册,具体代码如下.

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
// @@code-renderer: runner
// @@code-props: { height: '400px' }
// 注册自定义协议
const { app } = require('electron')
const path = require('path')

// 注册自定义协议
function setDefaultProtocol() {
const agreement = 'electron-playground-code' // 自定义协议名
let isSet = false // 是否注册成功

app.removeAsDefaultProtocolClient(agreement) // 每次运行都删除自定义协议 然后再重新注册
// 开发模式下在window运行需要做兼容
if (process.env.NODE_ENV === 'development' && process.platform === 'win32') {
// 设置electron.exe 和 app的路径
isSet = app.setAsDefaultProtocolClient(agreement, process.execPath, [
path.resolve(process.argv[1]),
])
} else {
isSet = app.setAsDefaultProtocolClient(agreement)
}
console.log('是否注册成功', isSet)
}

setDefaultProtocol()

使用协议

使用方式: 在浏览器地址栏输入注册好的协议,即可唤起应用。

协议唤起的链接格式: 自协议名称://参数

比如上文注册: electron-playground-code协议,触发时会默认带上://

在使用的时候, 需要在浏览器地址栏输入:

1
electron-playground-code://1234 // 1234是参数 可根据业务自行修改

1.6 注册协议并通过浏览器唤起后台应用的gif示例:

监听应用程序被唤起

应用程序唤起,mac系统会触发open-url事件,window系统会触发second-instance事件。

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
// @@code-renderer: runner
// @@code-props: { height: '450px' }
// 注册自定义协议
const { app, dialog } = require('electron')
const agreement = 'electron-playground-code' // 自定义协议名
// 验证是否为自定义协议的链接
const AGREEMENT_REGEXP = new RegExp(`^${agreement}://`)

// 监听自定义协议唤起
function watchProtocol() {
// mac唤醒应用 会激活open-url事件 在open-url中判断是否为自定义协议打开事件
app.on('open-url', (event, url) => {
const isProtocol = AGREEMENT_REGEXP.test(url)
if (isProtocol) {
console.log('获取协议链接, 根据参数做各种事情')
dialog.showMessageBox({
type: 'info',
message: 'Mac protocol 自定义协议打开',
detail: `自定义协议链接:${url}`,
})
}
})
// window系统下唤醒应用会激活second-instance事件 它在ready执行之后才能被监听
app.on('second-instance', (event, commandLine) => {
// commandLine 是一个数组, 唤醒的链接作为数组的一个元素放在这里面
commandLine.forEach(str => {
if (AGREEMENT_REGEXP.test(str)) {
console.log('获取协议链接, 根据参数做各种事情')
dialog.showMessageBox({
type: 'info',
message: 'window protocol 自定义协议打开',
detail: `自定义协议链接:${str}`,
})
}
})
})
}

// 在ready事件回调中监听自定义协议唤起
watchProtocol()
console.log('监听成功')

唤起应用执行回调示例

应用场景

  1. 单纯唤醒应用

    只需注册协议,系统会自动打开应用。
    表现:如果应用未打开将打开应用,如果应用已经打开应用将会激活应用窗口。

  2. 根据协议链接的参数进行各种操作

    如上面的弹窗演示, 在监听协议链接打开的时候,可以获取完整的协议链接

    我们可以根据协议链接来进行各种业务操作。

    比如跳转指定链接地址,比如判断是否登录再进行跳转,比如下载指定文件等。

一些其他API

app.removeAsDefaultProtocolClient(protocol) 删除注册的协议, 返回是否成功删除的Boolean

Mac: app.isDefaultProtocolClient(protocol) 当前程序是否为协议的处理程序。

app.getApplicationNameForProtocol(url) 获取该协议链接的应用处理程序

参数说明:

protocol 不包含:// 注册的协议名。

url 包含://

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// @@code-renderer: runner
// @@code-props: { height: '200px' }
// 自定义协议的其他相关API
const { app } = require('electron')
const agreement = 'electron-playground-code' // 自定义协议名

console.log('自行注释,自由尝试')
const isApp = app.isDefaultProtocolClient(agreement)
console.log('当前程序是否为自定义协议的处理程序: ', isApp)

const AgreementAppName = app.getApplicationNameForProtocol(`${agreement}://`)
console.log('获取该自定义协议链接的应用处理程序的名字', AgreementAppName)

const isDelete = app.removeAsDefaultProtocolClient(agreement)
console.log('删除自定义协议', isDelete)

自定义协议

注册自定义协议,拦截基于现有协议的请求,根据注册的自定义协议类型返回对应类型的数据。

在该项目中的代码地址: electron-playground/app/protocol, 可以运行项目调试一下看看效果。

protocol.registerSchemesAsPrivileged

将协议注册成标准的scheme, 方便后续调用。

注意: 它必须在ready事件加载之前调用,并且只能调用一次。

1
2
3
protocol.registerSchemesAsPrivileged([
{ scheme: 'myscheme', privileges: { bypassCSP: true } },
])

protocol.registerFileProtocol

拦截自定义协议的请求回调,重新处理后再请求路径。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol.registerFileProtocol(
'myscheme',
(request, callback) => {
// 拼接绝对路径的url
const resolvePath = path.resolve(__dirname, '../../playground')
let url = request.url.replace( `${myScheme}://`, '' )
url = `${resolvePath}/${url}`
return callback({ path: decodeURIComponent(url) })
},
error => {
if (error) console.error('Failed to register protocol')
},
)

PS: 在文档上提供了不同种类的加载API,这里只演示其中的一种。

使用方式

在html中使用自定义协议请求文件,即可自动拦截。

1
<img src={"myscheme://page/protocol/wakeUp.jpg"} alt="wakeUp"/>

protocol其他API

托盘

创建托盘

  1. 引入Tray类
  2. 获取图标地址
  3. 实例化tray并传入图标地址

代码如下:

1
2
3
4
5
6
// @@code-renderer: block
const { Tray } = require('electron')
const path = require('path')

const icon = path.join(__dirname, '你的图片路径')
new Tray(icon)

一个系统托盘就会被创建出来。很简单对不对,但是这个图标还没有任何功能,接下来我们为图标添加一些属性和事件。

设置托盘属性

常用属性和事件

为tray实例设置一些属性和事件,包括上下文菜单、鼠标移入文字。详细文档点击这里。

这里我们为tray设置灵活图标,让它可以根据系统主题显示不同的图标;再设置一个鼠标移入图标的时候会显示的提示文字,最后为它设置上下文菜单,让它可以具备一些功能。

先看下效果图:

附上代码:

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
// @@code-renderer: runner
// @@code-props: {height: '580px', hideRight: true}
const { Tray, Menu, nativeTheme, BrowserWindow } = require('electron')
const path = require('path')

let tray

// 设置顶部APP图标的操作和图标
const lightIcon = path.join(__dirname, '..', '..', 'resources', 'tray', 'StatusIcon_light.png')
const darkIcon = path.join(__dirname, '..', '..', 'resources', 'tray', 'StatusIcon_dark.png')

// 根据系统主题显示不同的主题图标
tray = new Tray(nativeTheme.shouldUseDarkColors ? darkIcon : lightIcon)

tray.setToolTip('Electron-Playground')

const contextMenu = Menu.buildFromTemplate([
{
label: '打开新窗口',
click: () => {
let child = new BrowserWindow({ parent: BrowserWindow.getFocusedWindow() })
child.loadURL('https://electronjs.org')
child.show()
},
},
{
label: '删除图标',
click: () => {
tray.destroy()
},
},
])

tray.setContextMenu(contextMenu)

你可以修改提示或者菜单来试一下。这里可以设置多个托盘,实际应用中要注意设置单例锁。

我们设置了托盘根据系统主题显示不同的图标,但是系统主题是动态的,又该怎么做呢,请看:

1
2
3
4
// @@code-renderer: block
nativeTheme.on('updated', () => {
tray.setImage(nativeTheme.shouldUseDarkColors ? darkIcon : lightIcon)
})

添加一个主题监听事件就好了。把这段代码复制到上面执行看下效果吧。

显示未读消息数(macOS)

在macOS系统下,可以采用setTitle(String)设置未读消息数。PS:windows下无效果。

1
2
// @@code-renderer: block
tray.setTitle("1")

效果是这样的:

你也可以复制到上面代码编辑器中然后点击执行看下效果。

有未读消息时图标闪动(windows)

在windows系统下,可通过setImage设置正常图标与空图标切换达到闪动效果。在mac系统下空图标不占用图标空间,所以需要设置透明图标。
你可以在下面示例中用darkIcon代替nativeImage.createEmpty()然后执行看一下效果。

如何判断操作系统平台,点击这里

windows下效果:

附代码:

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
// @@code-renderer: runner
// @@code-props: {height: '880px', hideRight: true}
const { Tray, Menu, nativeTheme, BrowserWindow, nativeImage } = require('electron')
const path = require('path')

let tray
let timer
let toggle = true
let haveMessage = true

const lightIcon = path.join(__dirname, '..', '..', 'resources', 'tray', 'StatusIcon_light.png')
const darkIcon = path.join(__dirname, '..', '..', 'resources', 'tray', 'StatusIcon_dark.png')

const win = BrowserWindow.getFocusedWindow();

tray = new Tray(lightIcon)

const contextMenu = Menu.buildFromTemplate([
{
label: '张三的消息',
click: () => {
let child = new BrowserWindow({ parent: BrowserWindow.getFocusedWindow() })
child.loadURL('https://electronjs.org')
child.show()
},
},
{ type: 'separator' },
{
label: '删除图标',
click: () => {
tray.destroy()
clearInterval(timer)
},
},
])

tray.setContextMenu(contextMenu)

tray.setToolTip('Electron-Playground')

if (haveMessage) {
timer = setInterval(() => {
toggle = !toggle
if (toggle) {
tray.setImage(nativeImage.createEmpty())
} else {
tray.setImage(lightIcon)
}
}, 600)
}

双击托盘显示隐藏界面(windows)

windows下效果:

附代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// @@code-renderer: runner
// @@code-props: {height: '260px', hideRight: true}
const { Tray, Menu, nativeTheme, BrowserWindow, nativeImage } = require('electron')
const path = require('path')

let tray

const lightIcon = path.join(__dirname, '..', '..', 'resources', 'tray', 'StatusIcon_light.png')

const win = BrowserWindow.getFocusedWindow()

tray = new Tray(lightIcon)

tray.on('double-click', () => {
win.isVisible() ? win.hide() : win.show()
})

注:此效果在windows上良好,在mac下会有兼容性问题,双击事件可能失效,实际使用过程中要注意。

菜单

简介

菜单主要分为应用程序菜单、上下文菜单,在tray和dock中也有用到菜单,本节主要介绍前两种。文档地址

以下案例如有操作到应用程序菜单,可点击右下角撤回恢复正常菜单。

应用程序菜单

mac和windows都在左上角,但是一个在屏幕左上角一个在应用程序视图左上角。
mac是这样的:

windows长这样:

如果windows下没有显示菜单,在当前窗口按alt键即会出现。

创建菜单

创建应用程序菜单

接下来我们创建应用程序菜单。如下步骤:

  1. 引入Menu类
  2. 定义一个菜单模板
  3. 调用Menu类的buildFromTemplate方法,该方法会根据传入的模板创建对应的菜单
  4. 调用Menu类的setApplicationMenu方法

此四步即可创建应用程序菜单,先来看下效果图。

附上代码:

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
// @@code-renderer: runner
// @@code-props: { height: '570px', hideRight: true }
const { Menu, dialog, app } = require('electron')

const template = [
{
label: 'app', // macOS下第一个标签是应用程序名字,此处设置无效
submenu: [
{ label: '退出', click: () => { app.quit() } },
{ label: '关于', click: () => { app.showAboutPanel() } }
]
},
{
label: '文件',
submenu: [
{
label: '子菜单',
click: () => {
// 调用了dialog(弹窗模块),演示效果
dialog.showMessageBoxSync({
type: 'info',
title: '提示',
message: '点击了子菜单'
})
}
}
]
}
]

const menu = Menu.buildFromTemplate(template)

Menu.setApplicationMenu(menu)

点击执行后看左上菜单效果。

创建上下文菜单

即创建右键点击菜单,前三步与创建应用程序菜单相同,最后一步需监听窗口context-menu事件展示菜单选项。
监听事件context-menu文档

先上效果图。

附上代码:

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
// @@code-renderer: runner
// @@code-props: {height: '460px', hideRight: true}
const { Menu, BrowserWindow } = require('electron')

const menu = new Menu();

const template = [
{
label: 'app', // macOS下第一个标签是应用程序名字,此处设置无效
submenu: [
{ role: 'quit' },
{ role: 'about' }
]
},
{
label: '编辑',
role: 'editMenu'
}
]

const contextMenu = Menu.buildFromTemplate(template)

// 主进程,渲染进程可使用window.addEventListener设置监听事件
BrowserWindow.getFocusedWindow().webContents.on('context-menu', () => {
contextMenu.popup()
})

可能你已经发现,这个例子的代码比上个例子少,实现的菜单却更多,而且这个role又是干嘛的呢,别急,往下看。

设置菜单属性

上节说到,这个role是干嘛的呢?
其实创建菜单行为有两种方式,一种是自定义,即1.1中实现方式,另外一种是预定义即role

role是MenuItem的属性,是electron的预定义行为。文档说:最好给任何一个菜单指定 role去匹配一个标准角色, 而不是尝试在 click 函数中手动实现该行为。 内置的 role 行为将提供最佳的原生体验。使用 role 时, label 和 accelerator 值是可选的, 并为每个平台,将默认为适当值。

这就是说,你只要设置好role属性,那么这个菜单的文案、快捷键、事件行为都已内部实现,而且比自定义的行为体验更好。

// @@code-renderer: runner
// @@code-props: {height: '500px', hideRight: true}
const { Menu, BrowserWindow } = require('electron')

const templateCustom = [
  {
    label: 'app', // macOS下第一个标签是应用程序名字,此处设置无效
    submenu: [
      { label: 'quit', role: 'quit' },
      {label: '关于', role: 'about', accelerator: 'CommandOrControl + shift + H' }
    ]
  },
  {
    label: '编辑',
    submenu: [
      {role: 'editMenu'},
      {type: 'separator'},
      {label: '自定义', click: () => {
        const win = new BrowserWindow()
        win.loadURL('https://electronjs.org')
      } }
    ]
  }
]

const customMenu = Menu.buildFromTemplate(templateCustom)

Menu.setApplicationMenu(customMenu)

line8的accelerator相信你看一眼就知道是设置快捷键的属性,你也可以自己更改快捷键点击执行试一下。

隐藏菜单