Skip to content

系统

系统中每个应用都或多或少地要与系统 API 打交道,比如显示系统通知、在系统托盘区域显示一个图标、通过“打开文件对话框”打开系统内一个指定的文件、通过“保存文件对话框”把数据保存到系统磁盘上等。
早期的 Electron(或 NW.js)对这方面支持不足,但随着使用者越来越多,用户需求也越来越多且各不相同,Electron 在这方面的支撑力度也变得越来越大。

系统对话框

使用系统文件对话框

在开发桌面应用时,经常会要求用户手动打开电脑上的某个文件或者把自己编辑的内容保存成某个文件,这类需求就需要通过系统对话框来实现。
打开文件时需要使用文件打开对话框,保存文件时需要使用文件保存对话框。
除此之外,还有路径选择对话框、消息提示对话框、错误提示对话框,这些都是系统对话框。

开发者使用传统的编程语言(如 C++ 或 Swift)开发桌面 GUI 应用时,一般会使用系统 API 来创建系统对话框。
但用 Electron 开发桌面应用则不必这么做,Electron 为我们包装了这些操作系统 API,开发者可以直接使用 JavaScript 来创建此类对话框,下面我们就重点介绍系统对话框的使用。

我们使用如下代码在渲染进程中创建一个文件打开对话框:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const { dialog, app } = require('electron').remote
let filePath = await dialog.showOpenDialog({
    title: "我需要打开一个文件",
    buttonLabel: "按此打开文件",
    defaultPath: app.getPath('pictures'),
    properties: "multiSelections",
    filters: [
    {name: "图片", extensions: ['jpg', 'png', 'gif']},
    {name: '视频', extensions: ['mkv', 'avi', 'mp4']}
    ]
})

运行程序,适时执行上面的代码,将打开图 8-1 所示的对话框。

图8-1文件打开对话框
图8-1 文件打开对话框

上述代码中使用主进程 dialog 对象的 showOpenDialog 方法来显示此对话框。

前面我们提到的文件打开对话框、文件保存对话框、路径选择对话框、消息提示对话框、错误提示对话框等都受 dialog 对象管理。

showOpenDialog 方法接受一个配置对象,该对象影响对话框的窗口元素和行为。在上图中需要重点关注的位置标记了编号:

  • ①处是对话框的标题,此处显示 title 属性的值。
  • ②处是对话框的默认路径,此处由 defaultPath 属性的值控制。
  • ③处是允许打开的文件类型,此处由 filters 属性的值控制。这里可以设置一个数组,以允许打开多种类型的文件。
  • ④处是确认按钮显示的文本,此处显示 buttonLabel 属性的值。

另外,properties 属性设置为 multiSelections,意为允许多选,此外还可以设置是否允许选择文件夹、是否只允许单选等。

showOpenDialog 返回一个 Promise 对象,当用户完成选择操作后,Promise 对象执行成功,结果值也为一个对象,这个对象包含两个属性:

  • canceled 属性,表示用户是否点击了此对话框的取消按钮
  • filePaths 属性,其类型为一个数组(因为通过设置可以让用户选择多个文件,所以此处为一个数组属性),里面的值是用户选择的文件的路径。不管用户有没有选择文件,只要点击了取消按钮,退出了对话框,此数组即为空。如果用户选择了一个文件或多个文件,点击确定按钮后此数组即为用户选择的文件路径,相应的数组内可能有一个或多个文件路径字符串。

拿到文件路径后,就可以对文件进行读写了

Electron 还提供了 showOpenDialogSync 方法,其与 showOpenDialog 的功用是相同的,但一个为同步版本,一个为异步版本。
使用 showOpenDialogSync 方法打开对话框后,如果用户迟迟没有选择文件,那么 JavaScript 执行线程将一直处于阻塞状态。
因此,一般情况下不推荐使用此方法

除了打开文件对话框外,Electron 还提供了保存文件对话框 dialog.showSaveDialog,显示消息对话框 dialog.showMessageBox 等

关于对话框

当应用部署到 Mac 或 Linux 系统上时,系统菜单中有一个 About[your app name]项,点击此菜单项后会弹出一个“关于”对话框。
这个对话框的内容如果不做指定,Electron 一般会按 package.json 配置默认生成一个。

开发者可以通过如下代码指定此对话框的内容

1
2
3
4
5
app.setAbountPanelOptions({
    applicationName: 'redredstar',
    applicationVersion: app.getVersion(),
    copyright: ''
})

设置完成后,“关于”对话框会显示你设置的内容,如图8-2所示。

图8-2Electron关于对话框

图8-2 Electron关于对话框

注意:开发环境下可能不会显示你的应用程序图标,而是显示Electron的图标,对于这一点不用担心,待你编译打包后,你的应用图标会自动出现在此对话框中。

如果你希望从应用的其他地方打开“关于”对话框,可以使用如下代码完成此操作:

1
app.showAboutPanel();

菜单

窗口菜单

使用 Electron 创建一个窗口,窗口默认会具备系统菜单,如图 8-3 所示。

图8-3Windows系统下窗口的菜单

图8-3 Windows系统下窗口的菜单

Electron 默认提供程序、编辑、视图、窗口、帮助等五个主菜单以及主菜单下的若干子菜单。
这些菜单主要用于演示,让开发者了解 Electron 在系统菜单方面有这些能力,实际意义不大。
比如 Help 菜单内的 Learn More 和 Document 等子菜单都还链接到 Electron 的官网,View 菜单下的 Reload 和 Zoom 菜单也不一定是应用所需要的功能。
开发者往往需要重新定制这些菜单。现在我们来了解如何在应用内使用 Electron 提供的系统菜单功能。

开发者可以在创建 Windows 窗口时通过设置 autoHideMenuBar 属性来隐藏菜单,代码如下所示:

1
2
3
4
let win = new BrowserWindow({
    webPreferences: { nodeIntegration: true },
    autoHideMenuBar:true        // 隐藏窗口的系统菜单
});

但用户打开窗口后,按一下 Alt 键菜单就又回来了(但在 Mac 系统下这项设置是没有用的,因为 Mac 系统下的菜单根本就不是显示在窗口内的)。

开发者一般还是会创建自己的系统菜单来覆盖 Electron 自带的菜单,代码如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
let Menu = require('electron').Menu;
let templateArr = [{
    label: "菜单1",
    submenu: [{ label: "菜单1-1" }, { label: "菜单1-2" }]
},{
    label: "菜单2",
    click() {
        console.log('hello menu')
    },
},{ label: "菜单3" },
{ label: "菜单4" }];
let menu = Menu.buildFromTemplate(templateArr);
Menu.setApplicationMenu(menu);

创建菜单需要使用 Electron 内置的 Menu 模块,通过 Menu.buildFromTemplate 方法来创建菜单对象,通过 Menu.setApplicationMenu 方法来为窗口设置系统菜单。设置成功后的菜单如图 8-4 所示。

图8-4自定义窗口菜单

图8-4 自定义窗口菜单

菜单配置对象中的 label 代表菜单显示的文本,可以通过 click 属性为菜单设置点击事件。
除此之外,还可以为菜单项设置 role 属性,如下代码所示:

1
{ label: "菜单3", role: 'paste' }

当设置此属性后,表明此菜单与粘贴行为相关,用户点击此菜单则执行操作系统的粘贴行为。
paste 外,role 属性的可选值还可以是 undo、redo、cut、copy、delete、selectAll、reload、minimize、close、quit 等,一个菜单项只能设置一个 role 值。

设置了 role 属性的菜单项后,再点击菜单时,其 click 事件将不再执行。

Windows 操作系统中不设置含 role 属性的菜单项并无大碍,但在 Mac 操作系统如果不设 cut、copy、paste 等属性的菜单项,窗口将不具备剪切、复制、和粘贴的能力,即使使用相关的快捷键也不能完成这些操作。

另外在 Mac 操作系统下,第一个菜单的位置要留出来,因为 Mac 操作系统会自动帮应用设置第一个菜单。
开发者可以判断当前系统是否为 Mac 系统,如果是,则在菜单模板中增加一个空菜单数据即可,如下代码所示:

1
2
3
if(process.platform === 'darwin'){
    templateArr.unshift({label:''})
}

上述代码中 label 的值为空,Mac 操作系统会使用应用名称替代此空值。

如果你希望菜单和菜单之间出现一个分隔条,那么可以在这两个菜单项之间加一行如下代码:

1
{ type: 'separator' }

上述代码表示分隔条是一个特殊的菜单项。

除此之外,你还可以把菜单项的 type 属性设置为 checkbox 或 radio,使其成为可被选中的菜单项。

HTML 右键菜单

在开发 Web 应用时,如果需要在页面上为用户提供右键菜单的功能,只需要三小段简短的代码即可实现。
第一段 HTML 代码,用户点右键看到的菜单内容即为此 HTM L所承载的内容:

1
2
3
4
5
6
7
8
<div id="menu">
    <div class="menu">功能1</div>
    <div class="menu">功能2</div>
    <div class="menu">功能3</div>
    <div class="menu">功能4</div>
    <div class="menu">功能5</div>
    <div class="menu">功能6</div>
</div>

第二段CSS代码,此样式代码用来格式化上述HTML代码所代表的菜单元素,默认情况下使右键菜单元素处于不可见状态:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#menu {
    width: 125px;
    overflow: hidden;
    border: 1px solid #ccc;
    box-shadow: 3px 3px 3px #ccc;
    position: absolute;
    display: none;
}
.menu {
    height: 36px;
    line-height: 36px;
    text-align: center;
    border-bottom: 1px solid #ccc;
    background: #fff;
}

第三段JS代码,通过此代码关联用户的行为与界面的响应:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
window.oncontextmenu = function(e) {
    e.preventDefault();
    var menu = document.querySelector("#menu");
    menu.style.left = e.clientX + "px";
    menu.style.top = e.clientY + "px";
    menu.style.display = "block";
};
window.onclick = function(e) {
    document.querySelector("#menu").style.display = 'none';
};

当用户在页面上点鼠标右键时,触发 window 对象的 oncontextmenu 事件,在此事件关联的方法内获得用户点击鼠标的位置,然后在该位置上显示前文所述右键菜单的 DOM 元素。
当用户点击页面上菜单外的任一位置时,隐藏右键菜单DOM元素。
也可以在Electron内使用此技术来创建右键菜单。
运行程序,在窗口页面任意位置点击右键,结果显示右键菜单,效果如图 8-5 所示。

然而用这种技术在Electron应用内创建菜单有一个弊端,这里创建的菜单只能显示在窗口页面内部,不能浮于窗口之上。
假设我们在窗口边缘处点击右键,将出现图8-6所示的效果。

图8-5HTML右键菜单

图8-5 HTML右键菜单

图8-6处于窗口边缘的HTML右键菜单

图8-6 处于窗口边缘的HTML右键菜单

如上图所示,菜单只显示了一半,而且页面出现了滚动条。
造成这个现象的原因是:菜单呈现后,页面实际宽度超出了窗口宽度,所以页面底部出现了滚动条,并且显示的菜单是页面内的 DOM 元素,这些DOM元素无法显示在窗口外部。
这种体验并不是很理想,使用系统菜单来与用户完成交互可以获得更好的效果。

系统右键菜单

我们在收到用户右键点击事件后,Electron 窗口内将显示可以浮在窗口上的系统菜单,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
let {Menu} = require('electron').remote;
let menu = Menu.buildFromTemplate([
    { label: "菜单1",
        click() {
                    alert("测试测试");
                } 
    },
    { label: "菜单2" },
    { label: "菜单3" },
    { label: "菜单4" }
]);
window.oncontextmenu = function(e) {
    e.preventDefault();
    menu.popup();
};

运行代码,在窗口边缘点击鼠标右键,发现弹出的菜单已经可以浮在窗口上面了,效果如图 8-7 所示。

图8-7系统右键菜单

图8-7 系统右键菜单

在窗口接收到 oncontextmenu 事件后,我们并没有为 menu 对象设置显示的位置,因为 menu 的 popup 方法规定菜单默认显示在当前窗口的鼠标所在位置。

自定义系统右键菜单

上面介绍的系统右键菜单方案也有一个不足:开发者很难定制系统右键菜单界面外观或在菜单项上添加额外的功能。
如果想达到这个目的,应考虑在鼠标点击右键时,在鼠标所在位置显示一个无边框窗口(一般来说还应禁用窗口的缩放功能),在此窗口内的页面上实现你的菜单外观和菜单点击事件。

注意,不应该使用 oncontextmenu 事件内的 e.clientXe.clientY 来获取鼠标位置,因为以这种方式获得的位置是相对于窗口的鼠标位置,而不是相对于屏幕的。
如果我们打算在点鼠标右键时显示一个窗口,那么此窗口的坐标应是相对于屏幕的,而不是相对于当前窗口的。应该通过以下方式获得鼠标位置:

1
2
3
4
5
6
window.oncontextmenu = function(e) {
    const { screen } = require("electron").remote;
    let point = screen.getCursorScreenPoint();        // 此为鼠标相对于屏幕的位置
    console.log(point);
    console.log(e.clientX + "," + e.clientY);
}

运行以上代码,你会发现打印出来的两个位置是有差异的。

还有一点需要注意,在菜单窗口显示出来后,除了在鼠标单击当前窗口其他元素时要关闭(或隐藏)这个菜单窗口外,在当前窗口失活的时候也要关闭(或隐藏)这个菜单窗口。监听窗口的失活事件代码如下:

1
2
3
win.on('blur', () => {
    menuWin.hide();
})

如果用户需要频繁地使用右键菜单窗口,建议你创建一个全局的菜单窗口,在用户使用它时控制它的位置和显隐状态,而不是每次都为用户新建一个窗口。
这样做可以提升菜单显示效率,节约系统资源。

另外,HTML5 提供了<menu>标签,根据MDN文档描述,它是可以显示在窗口之外的。
然而遗憾的是,此标签尚处于实验状态,目前几乎所有的浏览器都还不支持此特性。
可以关注此技术的进展,一旦Chrome浏览器支持,Electron 随之也会很快支持的。

快捷键

监听网页按钮事件

使用快捷键来操作应用程序往往比使用鼠标更快捷,比如按下Ctrl+S快捷键保存文档,按下Ctrl+F快捷键打开“查找”对话框等。

在Electron应用中不需要特殊支持,直接使用网页开发技术即可实现上述需求。比如,监听Ctrl+S快捷键的代码如下:

1
2
3
4
5
window.onkeydown = function() {
    if ((event.ctrlKey || event.metaKey) && event.keyCode == 83) {
        alert("按键监听");
    }
};

无论键盘上的什么键被按下(有些特殊按键除外),都会触发 window.onkeydown 事件,在此事件中我们过滤了指定的按键:

event.ctrlKey 代表Ctrl键,event.metaKey 代表Mac键盘的(花键),83(ASCII码)对应键盘上的S键,也就是说当Ctrl键或花键被按下,并且S键也同时被按下时,执行if条件内的逻辑。

注意此类快捷键只有在窗口处于激活状态时才可用,如需在窗口处于非激活状态时也能监听用户按键事件,则需要使用Electron的监听系统快捷键的能力

另外,你可以使用 mousetraphttps://github.com/ccampbell/mousetrap)作为按键事件监听库来监听网页按键事件,它做了很多封装工作,比如需要监听 *? 按键时,需要同时监听 Shift 按键,使用这个库就可以省略这个工作了。

监听全局按键事件

如果希望在窗口处于非激活状态时也能监听到用户的按键,应该使用 Electron 的 globalShortcut 模块,其代码如下:

1
2
3
4
const { globalShortcut } = require('electron')
globalShortcut.register('CommandOrControl+K', () => {
    console.log('按键监听')
})

只要你的 Electron 应用处于运行状态,无论窗口是否处于激活状态,上面的代码都能监听到 Ctrl+K(Mac系统下为Command+K)的按键事件。

CommandOrControl+K 是快捷键标记字符串,可以包含多个功能键和一个键码,功能键和键码用+号组合,比如:CommandOrControl+Shift+Z

如果你希望使用小键盘的快捷键,应在键码前加 num 字样,比如 CommandOrControl+num8,详情请查阅官方文档(https://electronjs.org/docs/api/accelerator)。

globalShortcut.register 必须在 app ready 事件触发后再执行,不然快捷键事件无法注册成功。

如果你注册的快捷键已经被另外一个应用注册过了,你的应用将无法注册此快捷键,也不会收到任何错误通知。你可以先通过 globalShortcut.isRegistered 方法来判断一下该快捷键是否已经被别的应用注册过了。

如果希望取消注册过的快捷键,可以使用 globalShortcut.unregister
globalShortcut 是一个主进程模块,可以在渲染进程中通过 require('electron').remote 访问它,但一定要注意渲染进程有可能重复注册事件的问题(页面刷新),这将导致主进程异常,进而监听不到按键事件。

托盘图标

托盘图标闪烁

实际生活中有很多应用程序需要常驻在用户的操作系统内,但用户又不希望其窗口一直显示在屏幕上。
也就是说,即使关掉程序的所有窗口,程序也要保持运行状态,例如QQ和微信等。
这时候开发者为了能让用户随时激活应用,打开窗口时就会在系统托盘处注册一个图标,来提示用户程序仍在运行状态,用户在点击图标时会打开程序的窗口,以回应用户的点击行为。
图 8-8 所示是我Windows操作系统下的托盘图标。

图8-8windows下的托盘图标

图8-8 windows下的托盘图标

Electron应用在系统托盘处注册一个图标十分简单,代码如下:

1
2
3
4
5
6
7
let { app, BrowserWindow, Tray } = require('electron');
let path = require('path');
let tray;
app.on('ready', () => {
    let iconPath = path.join(__dirname, 'icon.png');
    tray = new Tray(iconPath)
}

如上代码所示,在应用程序的 ready 事件中新建了一个 Tray 实例,并把图标文件的路径传递给了这个实例,该实例被赋值给一个全局对象(避免被垃圾收集),此时就可以在系统托盘显示应用程序的图标了。

QQ有一个有趣的特性,即有新消息时,QQ的托盘图标会闪烁。此特性的实现原理就是不断切换托盘图标。
Electron应用也可以轻松实现此特性,在上面代码块的 'ready' 事件结尾处,补充如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let iconPath2 = path.join(__dirname, 'icon2.png');
let flag = true;
setInterval(() => {
    if (flag) {
        tray.setImage(iconPath2);
        flag = false;
    } else {
        tray.setImage(iconPath);
        flag = true;
    }
}, 600)

上述代码被执行时,每隔600毫秒即切换一下图标,就实现了图标闪烁的效果。

如果希望做一个托盘图标的动画效果,那么可以多制作几个图标文件,在一个较短的时间内(一般情况下是每秒12帧以上)按顺序切换它们,这样就可以使应用程序的托盘图标以逐帧动画的形式播放了。
但出于性能损耗考虑,建议尽量少做几帧。

托盘图标菜单

仅有一个闪动的托盘图标并不能为应用提供任何实质性的帮助,实际上一个正常的托盘图标都应该接收用户点击事件,并完成打开应用窗口或者展现托盘图标菜单等工作。

使托盘图标响应鼠标点击事件很简单:

1
2
3
tray.on('click', function() {
    win.show();
})

在Windows或Mac系统中,还可以注册鼠标双击事件('double-click')或鼠标右键点击事件('right-click')。

重点:   在Electron应用里双击托盘图标,除了会触发'double-click'事件,也会触发'click'事件,非Electron应用则不一定具备此特性。

鼠标右键单击托盘图标,则只会触发'right-click'事件不会触发'click'事件。

一旦为托盘图标注册了菜单,则托盘图标将不再响应你注册的 'right-click' 事件。

下面是为托盘图标注册菜单的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let { Tray,Menu  } = require('electron');
let menu = Menu.buildFromTemplate([{
click() { win.show();  },
    label: '显示窗口',
    type: 'normal'
}, {
    click() { app.quit();  },
    label: '退出应用',
    type: 'normal'
}]);
tray.setContextMenu(menu);

如你所见,此处托盘图标使用的是系统菜单。菜单实例创建好后,只要通过 setContextMenu 方法绑定到托盘图标实例上即可。

剪切板

把图片写入剪切板

给系统剪切板写入一段文本或一段HTML代码非常简单,只要直接操作相应的API即可,代码如下:

1
2
3
let { clipboard } = require("electron");
clipboard.writeText("你好Text");                           // 向剪切板写入文本
clipboard.writeHTML("<b>你好HTML</b>");        // 向剪切板写入HTML

clipboard 模块是 Electron 中少有的几个主进程和渲染进程都可以直接使用的模块,给剪切板写入相应的内容后,只要按 Ctrl+V 快捷键,就可以把剪切板里的内容输出出来。

如果想给剪切板写入一张图片就没那么简单了,需要借助 Electron 提供的 nativeImage 模块才能实现,代码如下:

1
2
3
4
5
let path = require("path");
let { clipboard, nativeImage } = require("electron");
let imgPath = path.join(__static, "icon.png");
let img = nativeImage.createFromPath(imgPath);
clipboard.writeImage(img);

上面代码中,我们通过 nativeImage.createFromPath 创建了一个 nativeImage 对象,然后再把这个 nativeImage 对象写入剪切板。
代码运行后,如果在QQ聊天窗口按下Ctrl+V快捷键,会发现写入剪切板的图片已经出现在聊天窗口里了。

如果需要清除剪切板里的数据,可以使用如下代码:

1
clipboard.clear();

读取并显示剪切板里的图片

读取剪切板中的文本或HTML也非常简单,相关的API演示代码如下:

1
2
3
let { clipboard } = require("electron");
clipboard.readText();        // 读取剪切板的文本
clipboard.readHTML();        // 读取剪切板的HTML

读取并显示剪切板的图片,也要借助 Electron 的 nativeImage 模块,代码如下所示:

1
2
3
4
5
6
let { clipboard } = require("electron");
let img = clipboard.readImage();
let dataUrl = img.toDataURL();
let imgDom = document.createElement('img')
imgDom.src = dataUrl;
document.body.appendChild(imgDom);

clipboard.readImage 返回一个 nativeImage 的实例,此实例中的 toDataURL 方法返回图像的 base64 编码的数据字符串

开发者可以直接把此字符串设置到图片标签的src属性上,这样即可在网页中显示图片。

如果通过截图工具已经截取了图像数据到剪切板,那么执行上面的代码后,就会在窗口中看到截取的图片。

但如果在系统文件夹里复制了一个图片文件,执行上面代码,会看到一个加载失败的图片,因为此时剪切板里是一个文件而不是真正的图像数据(可以通过nativeImage 实例的 isEmpty 方法来判断 nativeImage 实例中是否包含图像数据)。
如果希望得到这个文件的路径,可以使用如下方法(这是一个Electron未公开的技术):

1
2
3
4
5
// Windows操作系统下
let filePath = clipboard.readBuffer('FileNameW').toString('ucs2')
filePath = filePath.replace(RegExp(String.fromCharCode(0), 'g'), '');
// Mac操作系统下
var filePath = clipboard.read('public.file-url').replace('file:// ', '');

除了这个方法外,还可以使用 clipboard-files 这个Node.js模块(https://github.com/alex8088/clipboard-files),它支持Windows和Mac两个平台。
这是一个原生组件,需要通过如下命令安装到Electron项目中:

1
> yarn add clipboard-files --build-from-source --runtime=electron --target=7.1.2 --target-arch=ia32 --dist-url=https:// atom.io/download/electron

安装完成后,可以通过如下代码获取剪切板内的文件路径:

1
2
const clipboard = require('clipboard-files');
let fileNames = clipboard.readFiles();

系统通知

使用 HTML API 发送系统通知

在开发网页时,如果需要使用系统通知,就要先获得用户授权。通过如下代码可以请求用户授权:

1
2
3
4
5
6
7
8
9
Notification.requestPermission(function (status) {
    if (status === "granted") {
        let notification = new Notification('您收到新的消息', {
            body: '此为消息的正文'
});
    } else {
        // 用户拒绝授权
    }
});

以上代码执行后会在网页上显示一个授权通知,如图8-10所示。

图8-10网页发送系统通知前的授权申请

图8-10 网页发送系统通知前的授权申请

用户点击允许后,回调函数 requestPer-mission 的参数 status 的值为 granted,此时网页向操作系统发送了一个通知,图 8-11 所示为系统显示通知的样例。

图8-11系统显示通知的样例

图8-11 系统显示通知的样例

Notification是一个HTML5的API,在Electron应用的渲染进程中也可以自由地使用它,而且不需要用户授权。
也就是说,Notification.requestPermission是多余的,开发者只要直接创建Notification的新实例就可以向用户系统发送通知。

系统显示通知后,如果用户点击通知,就会触发Notification类型的实例事件click:

1
2
3
notification.onclick = function(){
    alert('用户点击了系统消息');
}

主进程内发送系统通知

渲染进程可以使用HTML5的系统通知API。如果主进程需要给系统发送通知怎么办呢?
难道需要给渲染进程发送消息再调用渲染进程发送通知的逻辑吗?如果此时没有任何渲染进程在运行怎么办呢(应用程序没打开任何窗口)?
Electron的开发者考虑到了这些问题,并在主进程中创建了系统通知API。

主进程有Notification类型,它的大部分使用方法与HTML5的Notification类似,但有一个最主要的不同:HTML5的Notification实例创建之后会马上显示在用户系统的消息区域中;
但Electron主进程中的Notification实例创建之后,不会立刻向系统发送通知,而是需要调用其show方法才会显示系统通知,而且可以多次调用show方法,把同一个通知多次向系统发送。

在以下代码中,通过 remote 模块访问主进程中 Notification 的类型。
创建 Notifica-tion 实例时 title 不是作为独立的参数传递的,click 事件也不能使用 onclick 属性注册。

1
2
3
4
5
6
7
8
9
const { Notification } = require("electron").remote;
let notification = new Notification({
    title:"您收到新的消息",
    body: "此为消息的正文,点击查看消息",
});
notification.show();
notification.on("click", function() {
    alert("用户点击了系统消息");
});

其他

使用系统默认应用打开文件

在开发桌面应用时,我们经常会遇到一种需求,就是要根据不同的场景,启用系统的默认可执行程序
比如:用默认的Word应用程序打开一个Word文档,用默认浏览器打开一个URL链接等。
现在我们就介绍如何使用Electron的shell模块来处理这类需求。

shell模块可以被Electron中主进程和渲染进程直接使用,它的主要职责就是启动系统的默认应用。
下面是使用shell模块打开默认浏览器的代码:

1
2
const { shell } = require("electron");
shell.openExternal('https:// www.baidu.com');

以上代码执行后,系统会使用默认浏览器打开百度主页。
因为打开浏览器的过程是异步的,所以openExternal返回了一个Promise对象,你可以使用await关键字来等待浏览器打开之后再进行其他工作。
但是因为Electron无法判断默认浏览器是否成功打开了URL地址,所以这个 Promise 不包含有意义的返回值。

使用默认应用打开一个Word文档的代码如下:

1
2
const { shell } = require("electron");
let openFlag = shell.openItem("D:\\ 工作\\ Electron一线手记.docx")

你同样可以使用openItem方法打开其他已经在系统中注册了默认程序的文件,比如Excel或psd文件等。

openItem是一个同步方法,它返回一个布尔值,标记文件是否被成功打开了。
因为是同步方法,所以开发者应该注意此处阻塞JavaScript执行的问题

把一个文件移入垃圾箱的代码:

1
2
const { shell } = require("electron");
let delFlag = shell.moveItemToTrash("D:\\ 工作\\ Electron一线手记.docx");

moveItemToTrash也是同步方法,返回布尔值,标记方法是否成功执行(有时候文件被其他程序占用,是不能删除该文件的)。

另外shell模块还有创建和读取系统快捷方式(该API仅支持Windows平台),使系统发出哔哔声等功能,更多其他功能请参阅官方文档,这里不再介绍。

接收拖拽到窗口的文件

把文件拖放到应用程序窗口中也是一个比较常见的需求,HTML5提供了相应的事件支持,如下代码所示:

1
2
3
4
5
6
7
document.addEventListener('dragover', ev => {
    ev.preventDefault();
})
document.addEventListener('drop', ev => {
    console.log(ev.dataTransfer.files);
    ev.preventDefault();
})

你可以为某个具体DOM元素注册上面两个事件,当用户把文件拖拽到目标元素上时触发'dragover'事件,此时可以显示一些提示性文字,比如“请在此处放置文件”。

当用户在目标元素上释放鼠标时,触发 'drop' 事件,此时得到的 ev.dataTransfer.files 是一个File数组(因为用户可能一次拖拽进来多个文件)。

你可以使用HTML5的FileReader读取数组中的文件,代码如下:

1
2
3
4
5
6
7
8
let fr = new FileReader();
fr.onload = () => {
    var buffer = new Buffer.from(fr.result);
    fs.writeFile( newFilePath, buffer, err => { 
        // 文件保存完成
    });
};
fr.readAsArrayBuffer(fileObj);

上面的代码以二进制缓冲区数组的方式读取文件,文件读完之后,把文件内容保存到另一个地方(newFilePath)。
FileReader除了readAsArrayBuffer方法之外,还有表8-1中所示的方法。

表8-1 FileReader 读取文件对象的方法说明

方法 说明
readAsText 以文本方式读取文件,fr.result 即为文本内容
readAsDataURL 以 base64 方式读取文件,多用于读取图片,fr.result 为 base64 字符串
readAsBinaryString 以二进制字符串的方式读取文件

其实这里得到的File对象是有path属性的,path就是拖拽来的文件的绝对路径、拿到这个绝对路径之后,就可以利用Node.js的能力对它进行任意操作。

重点: 要想"drop"事件被正确触发,必须在'dragover'事件中通过preventDefault屏蔽掉浏览器的默认行为。

使用系统字体

在Web开发过程中,如果需要为网页设置字体,往往要罗列一大串字体名字,这样做是为了让网页在不同操作系统下都能用到最美观的字体,且能尽量表现一致。如下是ant design官网使用的字体样式:

1
font-family: Avenir,-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Hiragino Sans GB','Microsoft YaHei','Helvetica Neue',Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol',sans-serif;

但我们开发的是桌面GUI应用,大部分时候我们都希望应用看起来就像系统原生的应用一样,最好在什么操作系统下运行就使用什么操作系统的默认字体。
而且还有一个问题:操作系统下的原生应用使用的字体也不尽相同,比如标题栏用到的字体和状态栏用到的字体就不一样,菜单项用到的字体和对话框中用到的字体又不一样。

首先你想到的解决方案可能是:找出各操作系统在不同场景下都用了什么字体,然后为不同的操作系统编写不同的样式,在应用运行时判断当前处在什么操作系统内,然后根据操作系统名字来加载相应的样式文件。

这种方法固然可行,然而耗时耗力,得不偿失。另外一个更好的方案就是使用CSS3提供的系统字体支持来完成这项工作,代码如下:

1
2
3
4
<div style="font:caption">这是我的标题</div>
<div style="font:menu">菜单中的字体</div>
<div style="font:message-box">对话框中的字体</div>
<div style="font:status-bar">状态栏中的字体</div>

运行以上代码,界面显示如图8-12所示。

图8-12Electron使用系统字体示例

图8-12 Electron使用系统字体示例

font:caption 代表系统标题的字体,font:menu 代表菜单栏和菜单项的字体,font:message-box 代表消息提示的字体,font:status-bar 代表状态栏的字体,更多系统字体设置请参阅:https://developer.mozilla.org/zh-CN/docs/Web/CSS/font

font:caption 这种样式来控制界面中的字体,就可以直接满足上述需求了。上述代码最终被Electron翻译成如下样式:

1
2
3
4
<div style="font: 400 16px Arial;"> 这是我的标题 </div>
<div style="font: 400 12px &quot;Microsoft YaHei UI&quot;;"> 菜单中的字体 </div>
<div style="font: 400 16px Arial;"> 对话框中的字体 </div>
<div style="font: 400 12px &quot;Microsoft YaHei UI&quot;;"> 状态栏中的字体 </div>

如你所见,这个样式连字体的大小和粗细程度都为我们准备好了。
这其实并不是Electron的能力,在谷歌或火狐浏览器下运行上述代码可以得到同样的效果。
但Web开发人员一般不会把这个技术用在他们的网页中,导致这个技术就好像专门为Electron设计的一样。

扩展:  

如果你希望包括字体在内的所有界面元素都看起来像原生应用一样,那么你可以考虑在不同的系统下使用不同的设计语言。

比如在Windows系统下使用微软的Fluent设计语言(https://www.microsoft.com/design/fluent/#/web),此设计语言的前端实现项目称为Fabric,是一个基于React框架完成的项目,开源地址为:https://github.com/OfficeDev/office-ui-fabric-react。几乎所有的Office应用都在使用此项目。

在Mac系统下使用苹果公司的设计语言(https://developer.apple.com/design/human-interface-guidelines/macos/overview/themes/)。
我没有找到它的前端实现,不过语言定义非常清晰和完整。项目如果不是特别复杂的话,可以参照它的设计语言,自己实现前端代码。

如果你的应用需要发布到多个平台,那么兼容多平台的设计语言将是一个非常需要耐心和毅力的工作。

最近打开的文件

系统内很多程序都有“最近打开的文件”这个功能,比如Windows系统下的记事本,如图 8-13 中①处所示。

图8-13最近打开的文件示例

图8-13 最近打开的文件示例

Electron应用也有API支持实现此功能,如下代码所示:

1
app.addRecentDocument('C:\Users\Administrator\Desktop\1.jpg');

使用app的addRecentDocument方法,可以给应用增加一个最近打开的文件,此方法接收一个文件路径字符串参数。

相应的可以使用如下方法清空最近打开的文件列表:

1
app.clearRecentDocuments();

这两个方法都只对Mac或Windows操作系统有效,Linux系统没有这方面的能力。

在Windows系统中,需要做一些额外的操作才能让addRecentDocument有效。
需要把应用注册为某类文件的处理程序,否则应用不会显示最近打开的文件列表。把应用注册为某类文件的处理程序的方法请参阅微软的官方文档(https://docs.microsoft.com/zh-cn/windows/win32/shell/fa-intro?redirectedfrom=MSDN)。
当用户点击最近打开的文件列表中的某一项时,系统会启动一个新的应用程序的实例,而文件的路径将作为一个命令行参数被传入这个实例。

在Mac操作系统中,不需要为应用注册文件类型,当用户点击最近打开的文件列表中的某一项时,会触发app的open-file事件。
文件的路径会通过参数的形式传递给该事件的回调函数。