【Electron Playground 系列】窗口篇

作者:Kurosaki

本文主要講解Electron 窗口的 API 和一些在開發(fā)之中遇到的問(wèn)題。

官方文檔 雖然比較全面,但是要想開發(fā)一個(gè)商用級(jí)別的桌面應(yīng)用必須對(duì)整個(gè) Electron API 有較深的了解,才能應(yīng)對(duì)各種需求。

1. 創(chuàng)建窗口

通過(guò)BrowserWindow,來(lái) 創(chuàng)建 或者 管理 新的瀏覽器窗口,每個(gè)瀏覽器窗口都有一個(gè)進(jìn)程來(lái)管理。

1.1. 簡(jiǎn)單創(chuàng)建窗口

const { BrowserWindow } = require('electron');
const win = new BrowserWindow();
win.loadURL('https://github.com');

效果如下:

open-windows.gif

1.1.2. 優(yōu)化

問(wèn)題electronBrowserWindow模塊在創(chuàng)建時(shí),如果沒(méi)有配置 show:false,在創(chuàng)建之時(shí)就會(huì)顯示出來(lái),且默認(rèn)的背景是白色;然后窗口請(qǐng)求 HTML,會(huì)出現(xiàn)視覺(jué)閃爍。

解決

const { BrowserWindow } = require('electron');
const win = new BrowserWindow({ show:false });

win.loadURL('https://github.com');

win.on('ready-to-show',()=>{
    win.show();
})

兩者對(duì)比有很大的區(qū)別


window-shows.gif

1.2. 管理窗口

所謂的管理窗口,相當(dāng)于主進(jìn)程可以干預(yù)窗口多少。

  • 窗口的路由跳轉(zhuǎn)
  • 窗口打開新的窗口
  • 窗口大小、位置等
  • 窗口的顯示
  • 窗口類型(無(wú)邊框窗口、父子窗口)
  • 窗口內(nèi) JavaScriptnode 權(quán)限,預(yù)加載腳本等
  • ....

這些個(gè)方法都存在于BrowserWindow模塊中。

1.2.1. 管理應(yīng)用創(chuàng)建的窗口

BrowserWindow模塊在創(chuàng)建窗口時(shí),會(huì)返回 窗口實(shí)例,這些 **窗口實(shí)例 **上有許多功能方法,我們利用這些方法,管理控制這個(gè)窗口。

在這里使用Map對(duì)象來(lái)存儲(chǔ)這些 窗口實(shí)例。

const BrowserWindowsMap = new Map<number, BrowserWindow>()
let mainWindowId: number;

const browserWindows = new BrowserWindow({ show:false })
browserWindows.loadURL('https://github.com')
browserWindows.once('ready-to-show', () => {
  browserWindows.show()
})
BrowserWindowsMap.set(browserWindow.id, browserWindow)
mainWindowId = browserWindow.id  // 記錄當(dāng)前窗口為主窗口

窗口被關(guān)閉,得把Map中的實(shí)例刪除。

browserWindow.on('closed', () => {
  BrowserWindowsMap?.delete(browserWindowID)
})

1.2.2. 管理用戶創(chuàng)建的窗口

主進(jìn)程可以控制窗口許多行為,這些行為會(huì)在后續(xù)文章一一列舉;以下以主進(jìn)程控制窗口建立新窗口的行為為例。

使用new-window監(jiān)聽(tīng)新窗口創(chuàng)建

// 創(chuàng)建窗口監(jiān)聽(tīng)
browserWindow.webContents.on('new-window', (event, url, frameName, disposition) => {
  /** @params {string} disposition
  *  new-window : window.open調(diào)用
  *  background-tab: command+click
  *  foreground-tab: 右鍵點(diǎn)擊新標(biāo)簽打開或點(diǎn)擊a標(biāo)簽target _blank打開
  * /
})

注:關(guān)于disposition字段的解釋,移步electron文檔electron源碼、chrome 源碼

擴(kuò)展new-window

經(jīng)過(guò)實(shí)驗(yàn),并不是所有新窗口的建立, new-window 都能捕捉到的。

以下方式打開的窗口可以被new-window事件捕捉到

window.open('https://github.com')
<a  target='__blank'>鏈接</a>

**
渲染進(jìn)程中使用BrowserWindow創(chuàng)建新窗口,不會(huì)被 new-window事件捕捉到
**

const { BrowserWindow } = require('electron').remote
const win = new BrowserWindow()
win.loadURL('https://github.com')

_渲染進(jìn)程訪問(wèn) __remote_ _,主進(jìn)程需配置enableRemoteModule:true _
使用這種方式同樣可以打開一個(gè)新的窗口,但是主進(jìn)程的new-window捕捉不到。

應(yīng)用new-window
new-window 控制著窗口新窗口的創(chuàng)建,我們利用這點(diǎn),可以做到很多事情;比如鏈接校驗(yàn)、瀏覽器打開鏈接等等。默認(rèn)瀏覽器打開鏈接代碼如下:

import { shell } from 'electron'
function openExternal(url: string) {
  const HTTP_REGEXP = /^https?:\/\//
  // 非http協(xié)議不打開,防止出現(xiàn)自定義協(xié)議等導(dǎo)致的安全問(wèn)題
  if (!HTTP_REGEXP) {
    return false
  }
  try {
    await shell.openExternal(url, options)
    return true
  } catch (error) {
    console.error('open external error: ', error)
    return false
  }
}
// 創(chuàng)建窗口監(jiān)聽(tīng)
browserWindow.webContents.on('new-window', (event, url, frameName, disposition) => {
  if (disposition === 'foreground-tab') {
      // 阻止鼠標(biāo)點(diǎn)擊鏈接
      event.preventDefault()
      openExternal(url)
  }
})

_關(guān)于 __shell_ 模塊,可以查看官網(wǎng) https://www.electronjs.org/docs/api/shell
_

1.3. 關(guān)閉窗口

**close** **事件和 ****closed** 事件
close 事件在窗口將要關(guān)閉時(shí)之前觸發(fā),但是在 DOMbeforeunloadunload 事件之前觸發(fā)。

// 窗口注冊(cè)close事件
win.on('close',(event)=>{
    event.preventDefault()  // 阻止窗口關(guān)閉
})

closed 事件在窗口關(guān)閉后出觸發(fā),但是此時(shí)的窗口已經(jīng)被關(guān)閉了,無(wú)法通過(guò) event.preventDefault() 來(lái)阻止窗口關(guān)閉。

win.on('closed', handler)

主進(jìn)程能夠關(guān)閉窗口的 API 有很多,但都有各自的利弊。

1.3.1. win.close()

關(guān)于這個(gè) API 的利弊

  1. 如果當(dāng)前窗口實(shí)例注冊(cè)并阻止close事件,將不會(huì)關(guān)閉頁(yè)面,而且也會(huì) 阻止計(jì)算機(jī)關(guān)閉(必須手動(dòng)強(qiáng)制退出);
  2. 關(guān)閉頁(yè)面的服務(wù),如websocket,下次打開窗口,窗口中的頁(yè)面會(huì) 重新渲染
  3. 通過(guò)這個(gè)API觸發(fā)的close事件在 unloadbeforeunload之前觸發(fā),通過(guò)這點(diǎn)可以實(shí)現(xiàn) 關(guān)閉時(shí)觸發(fā)彈窗;

window-close.gif

完整代碼在github:electron-playground

  1. 會(huì)被closed事件捕捉到。

1.3.2. win.destroy()

  1. 強(qiáng)制退出,無(wú)視close事件(即:無(wú)法通過(guò)event.preventDefault()來(lái)阻止);
  2. 關(guān)閉頁(yè)面,以及頁(yè)面內(nèi)的服務(wù),下次打開窗口,窗口中的頁(yè)面會(huì)重新渲染;
  3. 會(huì)被closed事件捕捉到。

1.3.3. win.hide()

這個(gè)隱藏窗口。

  1. 隱藏窗口,會(huì)觸發(fā)hideblur事件,同樣也是可以通過(guò)event.preventDefault()來(lái)阻止
  2. 只是隱藏窗口,通過(guò)win.show(),可以將窗口顯現(xiàn),并且會(huì)保持原來(lái)的窗口,里面的服務(wù)也不會(huì)掛斷

2. 主窗口隱藏和恢復(fù)

2.1. 主窗口

2.1.1. 為什么需要 主窗口?

一個(gè)應(yīng)用存在著許多的窗口,需要一個(gè)窗口作為 主窗口,如果該窗口關(guān)閉,則意味著整個(gè)應(yīng)用被關(guān)閉。
場(chǎng)景:在應(yīng)用只有一個(gè)頁(yè)面的時(shí),用戶點(diǎn)擊關(guān)閉按鈕,不想讓整個(gè)應(yīng)用關(guān)閉,而是隱藏;
例如:其他的APP,像微信,QQ等桌面端。

利用上文中提到的關(guān)閉窗口的 API ,我們實(shí)現(xiàn)一個(gè)主窗口的隱藏和恢復(fù)。

改造一下 close 事件

let mainWindowId: number // 用于標(biāo)記主窗口id

const browserWindow = new BrowserWindow()

// 記錄下主窗口id
if (!mainWindowId) {
  mainWindowId = browserWindow.id
}

browserWindow.on('close', event => {
  // 如果關(guān)閉的是主窗口,阻止
  if (browserWindow.id === mainWindowId) {
    event.preventDefault()
    browserWindow.hide()
  }
})

2.1.2. 恢復(fù)主窗口顯示

能隱藏,就能恢復(fù)。

const mainWindow = BrowserWindowsMap.get(mainWindowId)
if (mainWindow) {
  mainWindow.restore()
  mainWindow.show()
}

**mainWindow.show()** 方法:功能如其名,就是“show出窗口”。
_為什么要是有 __mainWindow.restore()_
_windows_ _下如果 __hide_ _之后不調(diào)用 __show_ _方法而是只調(diào)用 __restore_ 方法就會(huì)導(dǎo)致頁(yè)面掛住不能用

2.1.3. 強(qiáng)制關(guān)閉主窗口

有些場(chǎng)景下,可能需要的強(qiáng)制退出,附上代碼:

const mainWindow = BrowserWindowsMap.get(mainWindowId)
if (mainWindow) {
  mainWindowId = -1
  mainWindow.close()
}

存在的問(wèn)題

我們改變了 Electron 窗口的既定行為,就會(huì)有許多場(chǎng)景下會(huì)有問(wèn)題

問(wèn)題一:因?yàn)樽柚沽?close 事件,導(dǎo)致 關(guān)機(jī) 時(shí)無(wú)法關(guān)閉 主窗口,可以使用如下代碼

app.on('before-quit', () => {
    closeMainWindow()
})

在 macOS Linux Windows 下都可以。

問(wèn)題二:為避免啟動(dòng) 多個(gè)應(yīng)用

app.on('second-instance', () => {
  const mainWindow = BrowserWindowsMap.get(mainWindowId)
  if (mainWindow) {
    mainWindow.restore()
    mainWindow.show()
  }
})

在 macOS Linux Windows 下都可以

問(wèn)題三:首次啟動(dòng)應(yīng)用程序、嘗試在應(yīng)用程序已運(yùn)行時(shí)或單擊 應(yīng)用程序塢站任務(wù)欄圖標(biāo) 時(shí)重新激活它

app.on('activate', () => {
  if (mainWindow) {
    mainWindow.restore()
    mainWindow.show()
  }
})

只應(yīng)用于macOS

問(wèn)題四: 雙擊托盤圖標(biāo) 打開APP

tray.on('double-click', () => {
  if (mainWindow) {
    mainWindow.restore()
    mainWindow.show()
  }
})

這樣每個(gè)環(huán)節(jié)的代碼都有,即可實(shí)現(xiàn),具體代碼可參見(jiàn)鏈接

3. 窗口的聚焦和失焦

3.1. 聚焦

3.1.1. 創(chuàng)建窗口時(shí)配置

const { BrowserWindow } = require('electron');
const win = new BrowserWindow();
win.loadURL('https://github.com')

focusable:true 窗口便可聚焦,便可以使用聚焦的 API
focusable:falseWindows 中設(shè)置 focusable: false 也意味著設(shè)置了skipTaskbar: true. 在 Linux 中設(shè)置 focusable: false 時(shí)窗口停止與 wm 交互, 并且窗口將始終置頂;

以下討論的情況僅為focusable:true情況下

const { BrowserWindow } = require('electron');
const win = new BrowserWindow() // focusable:true 為默認(rèn)配置

羅列了一下 API

3.1.2. 關(guān)于聚焦的API

API 功能
BrowserWindow.getFocusedWindow() 來(lái)獲取聚焦的窗口
win.isFocused() 判斷窗口是否聚焦
win.on('focus',handler) 來(lái)監(jiān)聽(tīng)窗口是否聚焦
win.focus() 手動(dòng)聚焦窗口

3.1.3. 其他API副作用和聚焦有關(guān)的:

API 功能
win.show() 顯示窗口,并且聚焦于窗口
win.showInactive() 顯示窗口,但是不會(huì)聚焦于窗口

3.2. 失焦

3.2.1. 關(guān)于失焦的api

API 功能
win.blur() 取消窗口聚焦
win.on('blur',cb) 監(jiān)聽(tīng)失焦

3.2.2. 其他api副作用和失焦有關(guān)的:

api 功能
win.hide() 隱藏窗口,并且會(huì)觸發(fā)失焦事件

4. 窗口類型

4.1. 無(wú)邊框窗口

4.1.1. 描述

無(wú)邊框窗口是不帶外殼(包括窗口邊框、工具欄等),只含有網(wǎng)頁(yè)內(nèi)容的窗口

4.1.2. 實(shí)現(xiàn)

Windows macOS Linux

const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ width: 800, height: 600, frame: false })
win.show()

macOS下,還有不同的實(shí)現(xiàn)方式,官方文檔

4.1.3. macOS 下獨(dú)有的無(wú)邊框

  • 配置titleBarStyle: 'hidden'

返回一個(gè)隱藏標(biāo)題欄的全尺寸內(nèi)容窗口,在左上角仍然有標(biāo)準(zhǔn)的窗口控制按鈕(俗稱“紅綠燈”)

// 創(chuàng)建一個(gè)無(wú)邊框的窗口
const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ titleBarStyle: 'hidden' })
win.show()

效果如下:


window-type-frame.gif
  • 配置titleBarStyle: 'hiddenInset'

返回一個(gè)另一種隱藏了標(biāo)題欄的窗口,其中控制按鈕到窗口邊框的距離更大。

// 創(chuàng)建一個(gè)無(wú)邊框的窗口
const { BrowserWindow } = require('electron')
let win = new BrowserWindow({ titleBarStyle: 'hiddenInset' })
win.show()

效果如下:


window-type-frame2.gif
配置titleBarStyle: 'customButtonsOnHover'

效果如下:


window-type-frame3.gif

4.1.4. 窗口頂部無(wú)法拖拽的問(wèn)題

雖然無(wú)邊框窗口,很美觀,可以自定義title;但是改變了Electron窗口頂部的默認(rèn)行為,就需要使用代碼來(lái)兼容它,實(shí)現(xiàn)其原來(lái)承擔(dān)的功能。

window-type1.gif

出現(xiàn)上述情況,是因?yàn)樵谀J(rèn)情況下, 無(wú)邊框窗口是不可拖拽的。 應(yīng)用程序需要在 CSS 中指定 -webkit-app-region: drag 來(lái)告訴 Electron 哪些區(qū)域是可拖拽的(如操作系統(tǒng)的標(biāo)準(zhǔn)標(biāo)題欄),在可拖拽區(qū)域內(nèi)部使用 -webkit-app-region: no-drag 則可以將其中部分區(qū)域排除。 請(qǐng)注意, 當(dāng)前只支持矩形形狀。完整文檔

使用-webkit-app-region: drag 來(lái)實(shí)現(xiàn)拖拽,但是會(huì)導(dǎo)致內(nèi)部的click事件失效。這個(gè)時(shí)候可以將需要click元素設(shè)置為-webkit-app-region: no-drag。具體的細(xì)節(jié) Electronissues

為了不影響窗口內(nèi)的業(yè)務(wù)代碼,這里拖拽的代碼,應(yīng)該在preload觸發(fā)。

preload 代碼運(yùn)行,在窗口代碼運(yùn)行之前

核心代碼:

// 在頂部插入一個(gè)可以移動(dòng)的dom
function initTopDrag() {
  const topDiv = document.createElement('div') // 創(chuàng)建節(jié)點(diǎn)
  topDiv.style.position = 'fixed' // 一直在頂部
  topDiv.style.top = '0'
  topDiv.style.left = '0'
  topDiv.style.height = '20px' // 頂部20px才可拖動(dòng)
  topDiv.style.width = '100%' // 寬度100%
  topDiv.style.zIndex = '9999' // 懸浮于最外層
  topDiv.style.pointerEvents = 'none' // 用于點(diǎn)擊穿透
  // @ts-ignore
  topDiv.style['-webkit-user-select'] = 'none' // 禁止選擇文字
  // @ts-ignore
  topDiv.style['-webkit-app-region'] = 'drag' // 拖動(dòng)
  document.body.appendChild(topDiv) // 添加節(jié)點(diǎn)
}

window.addEventListener('DOMContentLoaded', function onDOMContentLoaded() {
    initTopDrag()
})

在創(chuàng)建窗口時(shí)引用 preload 即可

const path = require('path')
const { BrowserWindow } = require('electron')

const BaseWebPreferences = {
  nodeIntegration: true,
  preload: path.resolve(__dirname, './windowType.js'), // 這里引用preload.js 路徑
}

// 主窗口代碼
const win = new BrowserWindow({ webPreferences: BaseWebPreferences, frame: false, titleBarStyle: 'hiddenInset' })
win.loadURL('https://github.com')

便可實(shí)現(xiàn)窗口頂部拖拽

window-type.gif

_tips: 如果窗口打開了 __devtools_ ,窗口也是可以拖拽的,只不過(guò)這個(gè)拖拽體驗(yàn)不好

4.2. 父子窗口

所謂的父子窗口,就是子窗口永遠(yuǎn)在父窗口之上,只要子窗口存在,哪怕位置不在父窗口上方,都是無(wú)法操作父窗口

window-type2.gif
const { BrowserWindow } = require('electron')

let top = new BrowserWindow()
let child = new BrowserWindow({ parent: top })
child.show()
top.show()

窗口之間通信 章節(jié)中介紹到父子窗口之間的通信;通過(guò) getParentWindow 拿到父窗口的 類BrowserWindowProxy,通過(guò) win.postMessage(message,targetOrigin) 實(shí)現(xiàn)通信

4.3. 模態(tài)窗口

模態(tài)窗口也是一種父子窗口,只不過(guò)展示會(huì)有不同

const { BrowserWindow } = require('electron')

let top = new BrowserWindow()
let child = new BrowserWindow({ parent: top, modal: true, show: false })
child.loadURL('https://github.com')
child.once('ready-to-show', () => {
  child.show()
})
window-type3.gif

5. 窗口之間的通信

實(shí)現(xiàn)窗口通信必須不影響窗口內(nèi)的業(yè)務(wù)代碼, jdk 等的注入

5.1. 主進(jìn)程干預(yù)方式

主進(jìn)程是可以干預(yù)渲染進(jìn)程生成新的窗口的,只需要在創(chuàng)建窗口時(shí),webContents 監(jiān)聽(tīng) new-window

import path from 'path'
import { PRELOAD_FILE } from 'app/config'
import { browserWindow } from 'electron';

const BaseWebPreferences: Electron.BrowserWindowConstructorOptions['webPreferences'] = {
  nodeIntegration: true,
  webSecurity: false,
  preload: path.resolve(__dirname, PRELOAD_FILE),
}


// 創(chuàng)建窗口監(jiān)聽(tīng)
browserWindow.webContents.on('new-window', (event, url, frameName, disposition) => {
    event.preventDefault()
    // 在通過(guò)BrowserWindow創(chuàng)建窗口
    const win = new BrowserWindow({ 
      show:false, 
      webPreferences: {
        ...BaseWebPreferences,
        additionalArguments:[`--parentWindow=${browserWindow.id}`] // 把父窗口的id傳過(guò)去
      } 
    });
    win.loadURl(url);
    win.once('ready-to-show',()=>{
        win.show()
    })
})

preload.js 文件window.process.argv,便能拿到父窗口的id,window.process.argv是一個(gè)字符串?dāng)?shù)組,可以使用yargs來(lái)解析

preload.js 代碼

import { argv } from 'yargs'
console.log(argv);

yargv-parse.png

拿到了父窗口的 id ,封裝一下通信代碼,掛載到 window

/**
 * 這個(gè)是用于窗口通信例子的preload,
 * preload執(zhí)行順序在窗口js執(zhí)行順序之前
 */
import { ipcRenderer, remote } from 'electron'
const { argv } = require('yargs')

const { BrowserWindow } = remote

// 父窗口監(jiān)聽(tīng)子窗口事件
ipcRenderer.on('communication-to-parent', (event, msg) => {
  alert(msg)
})

const { parentWindowId } = argv
if (parentWindowId !== 'undefined') {
  const parentWindow = BrowserWindow.fromId(parentWindowId as number)
  // 掛載到window
  // @ts-ignore
  window.send = (params: any) => {
    parentWindow.webContents.send('communication-to-parent', params)
  }
}

應(yīng)用一下試試看:


window-com.gif

這種方法可以實(shí)現(xiàn)通信,但是太麻煩了。

5.2. 父子窗口通信

和主進(jìn)程干預(yù),通過(guò)ipc通信方式差不多,只是利用父子窗口這點(diǎn),不用通過(guò)additionalArguments傳遞父窗口id,在子窗口通過(guò)window.parent,就可以拿到父窗口

browserWindow.webContents.on('new-window', (event, url, frameName, disposition) => {
    event.preventDefault()
      
    // 在通過(guò)BrowserWindow創(chuàng)建窗口
    const win = new BrowserWindow({ 
        show:false, 
        webPreferences:BaseWebPreferences,
        parent:browserWindow // 添加父窗口
      });
    win.loadURl(url);
    win.once('ready-to-show',()=>{
        win.show()
    })
    
})

弊端:子窗口永遠(yuǎn)在父窗口之上。

const path = require('path')
const { BrowserWindow } = require('electron')

const BaseWebPreferences = {
  // // 集成node
  nodeIntegration: true,
  // // 禁用同源策略
  // webSecurity: false,
  // 預(yù)加載腳本 通過(guò)絕對(duì)地址注入
  preload: path.resolve(__dirname, './communication2.js'),
}

// 主窗口代碼
const parent = new BrowserWindow({ webPreferences: BaseWebPreferences, left: 100, top: 0 })
parent.loadURL(
  'file:///' + path.resolve(__dirname, '../playground/index.html#/demo/communication-part2/main'),
)
parent.webContents.on('new-window', (event, url, frameName, disposition) => {
  // 阻止默認(rèn)事件
  event.preventDefault()
  // 在通過(guò)BrowserWindow創(chuàng)建窗口
  // 子窗口代碼
  const son = new BrowserWindow({
    webPreferences: BaseWebPreferences,
    parent,
    width: 400,
    height: 400,
    alwaysOnTop: false,
  })
  // son.webContents.openDevTools();
  son.loadURL(
    'file:///' +
      path.resolve(__dirname, '../playground/index.html#/demo/communication-part2/client'),
  )
})

preload.js

import { remote, ipcRenderer } from 'electron'

// 父窗口監(jiān)聽(tīng)子窗口事件
ipcRenderer.on('communication-to-parent', (event, msg) => {
  alert(msg)
})

const parentWindow = remote.getCurrentWindow().getParentWindow()
// @ts-ignore
window.sendToParent = (params: any) =>
  parentWindow.webContents.send('communication-to-parent', params)

window-com1.gif

但是必須得是父子窗口,有弊端。

5.3. 使用window.open

終極方法

web 端,使用 window.open 會(huì)返回一個(gè) windowObjectReference ,通過(guò)這個(gè)方法可以實(shí)現(xiàn) postMessage ;但是在 Electron 端,把 window.open 方法重新定義了;使用 window.open 創(chuàng)建一個(gè)新窗口時(shí)會(huì)返回一個(gè) BrowserWindowProxy對(duì)象,并提供一個(gè)有限功能的子窗口.
MDN文檔 Electron文檔

const  BrowserWindowProxy = window.open('https://github.com', '_blank', 'nodeIntegration=no')
BrowserWindowProxy.postMessage(message, targetOrigin)

代碼精簡(jiǎn),且需要的功能,即符合 BrowserWindow(options)options 配置的,都可以使用 window.open 配置。

6. 全屏、最大化、最小化、恢復(fù)

6.1. 全屏

6.1.1. 創(chuàng)建時(shí)進(jìn)入全屏

配置new BrowserWindow({ fullscreen:true })

const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ fullscreen:true,fullscreenable:true })
win.loadURL('https://github.com')

6.1.2. 使用API進(jìn)入全屏

確保當(dāng)前窗口的fullscreenable:true,以下API才能使用

  1. win.setFullScreen(flag),設(shè)置全屏狀態(tài);
  2. win.setSimpleFullScreen(flag),macOS下獨(dú)有,設(shè)置簡(jiǎn)單全屏。

6.1.3. 全屏狀態(tài)的獲取

  1. win.fullScreen,來(lái)判斷當(dāng)前窗口是否全屏;
  2. win.isFullScreen()macOS獨(dú)有;
  3. win.isSimpleFullScreen()macOS獨(dú)有。

6.1.4. 全屏事件的監(jiān)聽(tīng)

  1. rezise 調(diào)整窗口大小后觸發(fā);
  2. enter-full-screen 窗口進(jìn)入全屏狀態(tài)時(shí)觸發(fā);
  3. leave-full-screen 窗口離開全屏狀態(tài)時(shí)觸發(fā);
  4. enter-html-full-screen 窗口進(jìn)入由HTML API 觸發(fā)的全屏狀態(tài)時(shí)觸發(fā);
  5. leave-html-full-screen 窗口離開由HTML API觸發(fā)的全屏狀態(tài)時(shí)觸發(fā)。

6.1.5. HTML API無(wú)法和窗口聯(lián)動(dòng)問(wèn)題

const path = require('path')
const { BrowserWindow } = require('electron')
const BaseWebPreferences = { 
    nodeIntegration: true,
    preload: path.resolve(__dirname, './fullScreen.js'), 
};
const win = new BrowserWindow({ webPreferences: BaseWebPreferences })
win.loadURL('file:///' + path.resolve(__dirname, '../playground/index.html#/demo/full-screen'))

使用按鈕全屏和退出全屏是可以的,但是先點(diǎn)擊左上角??全屏,再使用按鈕退出全屏,是不行的。因?yàn)闊o(wú)法知道當(dāng)前的狀態(tài)是全屏,還是不是全屏。

解決辦法:,將win.setFullScreen(flag)方法掛載到窗口的window
加載這樣一段preload.js代碼即可

import { remote } from 'electron'

const setFullScreen = remote.getCurrentWindow().setFullScreen
const isFullScreen = remote.getCurrentWindow().isFullScreen

window.setFullScreen = setFullScreen
window.isFullScreen = isFullScreen

_ setFullScreen文檔 https://www.electronjs.org/docs/api/browser-window#winsetfullscreenflag isFullScreen 文檔_https://www.electronjs.org/docs/api/browser-window#winisfullscreen

6.2. 最大化、最小化

6.2.1. 創(chuàng)建窗口配置

完整API文檔

const { BrowserWindow } = require('electron')
const win = new BrowserWindow({ minWidth:300,minHeight:300,maxWidth:500,maxHeight:500,width:600,height:600 })
win.loadURL('https://github.com')

當(dāng)使用 minWidth/maxWidth/minHeight/maxHeight 設(shè)置最小或最大窗口大小時(shí), 它只限制用戶。 它不會(huì)阻止您將不符合大小限制的值傳遞給 setBounds/setSizeBrowserWindow 的構(gòu)造函數(shù)。

6.2.2. 相關(guān)事件

事件名稱 觸發(fā)條件
maximize 窗口最大化時(shí)觸發(fā)
unmaximize 當(dāng)窗口從最大化狀態(tài)退出時(shí)觸發(fā)
minimize 窗口最小化時(shí)觸發(fā)
restore 當(dāng)窗口從最小化狀態(tài)恢復(fù)時(shí)觸發(fā)

6.2.3. 相關(guān)狀態(tài)API

  1. win.minimizable 窗口是否可以最小化
  2. win.maximizable 窗口是否可以最大化
  3. win.isMaximized() 是否最大化
  4. win.isMinimized() 是否最小化

6.2.4. 控制API

  1. win.maximize() 使窗口最大化
  2. win.unmaximize() 退出最大化
  3. win.minimize() 使窗口最小化
  4. win.unminimize() 退出最小化

6.3. 窗口恢復(fù)

win.restore() 將窗口從最小化狀態(tài)恢復(fù)到以前的狀態(tài)。在前面的例子 主窗口隱藏和恢復(fù)也有用到這個(gè)api

7. 窗口各事件觸發(fā)順序

window-event.png

7.1. 窗口加載時(shí)

BrowserWindow實(shí)例:即 new BrowserWindow() 返回的實(shí)例對(duì)象
webContents: 即 BrowserWindow 實(shí)例中的 webContents 對(duì)象
webPreferences: 即 new BrowserWindow(options)optionswebPreferences 配置對(duì)象

從上到下,依次執(zhí)行

環(huán)境 事件 觸發(fā)時(shí)機(jī)
webPreferences的preload - 在頁(yè)面運(yùn)行其他腳本之前預(yù)先加載指定的腳本 無(wú)論頁(yè)面是否集成Node, 此腳本都可以訪問(wèn)所有Node API 腳本路徑為文件的絕對(duì)路徑。
webContents did-start-loading 當(dāng)tab中的旋轉(zhuǎn)指針(spinner)開始旋轉(zhuǎn)時(shí),就會(huì)觸發(fā)該事件
webContents did-start-navigation 當(dāng)窗口開始導(dǎo)航是,觸發(fā)該事件
窗口中的JavaScript DOMContentLoaded 初始的 HTML 文檔被完全加載和解析完成
窗口中的JavaScript load 頁(yè)面資源全部加載完成之時(shí)
BrowserWindow實(shí)例 show 窗口顯示時(shí)觸發(fā)時(shí)
webContents did-frame-navigate frame導(dǎo)航結(jié)束時(shí)時(shí)
webContents did-navigate main frame導(dǎo)航結(jié)束時(shí)時(shí)
BrowserWindow實(shí)例 page-title-updated 文檔更改標(biāo)題時(shí)觸發(fā)
webContents page-title-updated 文檔更改標(biāo)題時(shí)觸發(fā)
webContents dom-ready 一個(gè)框架中的文本加載完成后觸發(fā)該事件
webContents did-frame-finish-load 當(dāng)框架完成導(dǎo)航(navigation)時(shí)觸發(fā)
webContents did-finish-load 導(dǎo)航完成時(shí)觸發(fā),即選項(xiàng)卡的旋轉(zhuǎn)器將停止旋轉(zhuǎn)
webContents did-stop-loading 當(dāng)tab中的旋轉(zhuǎn)指針(spinner)結(jié)束旋轉(zhuǎn)時(shí),就會(huì)觸發(fā)該事件

7.2. 窗口加載完畢,用戶觸發(fā)事件(不包括resize和move)

事件 作用
page-title-updated 文檔更改標(biāo)題時(shí)觸發(fā)
blur 當(dāng)窗口失去焦點(diǎn)時(shí)觸發(fā)
focus 當(dāng)窗口獲得焦點(diǎn)時(shí)觸發(fā)
hide 窗口隱藏
show 窗口顯示
maximize 窗口最大化時(shí)觸發(fā)(mac是雙擊title)
unmaximize 當(dāng)窗口從最大化狀態(tài)退出時(shí)觸發(fā)
enter-full-screen 窗口進(jìn)入全屏狀態(tài)時(shí)觸發(fā)
leave-full-screen 窗口離開全屏狀態(tài)時(shí)觸發(fā)
enter-html-full-screen 窗口進(jìn)入由HTML API 觸發(fā)的全屏狀態(tài)時(shí)觸發(fā)
leave-html-full-screen 窗口離開由HTML API觸發(fā)的全屏狀態(tài)時(shí)觸發(fā)
always-on-top-changed 設(shè)置或取消設(shè)置窗口總是在其他窗口的頂部顯示時(shí)觸發(fā)。
app-command window linux獨(dú)有

7.3. 用戶移動(dòng)窗口

  1. 移動(dòng)窗口之前 will-move
  2. 移動(dòng)窗口中 move;
  3. 移動(dòng)之后 moved

7.4. 用戶改變窗口大小

  1. 改變之前 will-resize;
  2. 改變之后 resize

7.5. 窗口的內(nèi)容異常事件(webContent事件)

事件名 錯(cuò)誤類型
unresponsive 網(wǎng)頁(yè)變得未響應(yīng)時(shí)觸發(fā)
responsive 未響應(yīng)的頁(yè)面變成響應(yīng)時(shí)觸發(fā)
did-fail-load 加載失敗,錯(cuò)誤碼
did-fail-provisional-load 頁(yè)面加載過(guò)程中,執(zhí)行了window.stop()
did-frame-finish-load
crashed 渲染進(jìn)程崩潰或被結(jié)束時(shí)觸發(fā)
render-process-gone 渲染進(jìn)程意外失敗時(shí)發(fā)出
plugin-crashed 有插件進(jìn)程崩潰時(shí)觸發(fā)
certificate-error 證書的鏈接驗(yàn)證失敗
preload-error preload.js拋出錯(cuò)誤

7.6. 窗口關(guān)閉(包括意外關(guān)閉)

  • 關(guān)閉之前:觸發(fā)主進(jìn)程中注冊(cè)的 close 事件
  • 窗口內(nèi)的 JavaScript 執(zhí)行 window.onbeforeunload
  • 窗口內(nèi)的 JavaScript 執(zhí)行 window.onunload
  • 關(guān)閉之后:觸發(fā)主進(jìn)程中注冊(cè)的 closed 事件

對(duì) Electron 感興趣?請(qǐng)關(guān)注我們的開源項(xiàng)目 Electron Playground,帶你極速上手 Electron。

我們每周五會(huì)精選一些有意思的文章和消息和大家分享,來(lái)掘金關(guān)注我們的 曉前端周刊。


我們是好未來(lái) · 曉黑板前端技術(shù)團(tuán)隊(duì)。
我們會(huì)經(jīng)常與大家分享最新最酷的行業(yè)技術(shù)知識(shí)。
歡迎來(lái) 知乎掘金SegmentfaultCSDN、簡(jiǎn)書、開源中國(guó)博客園 關(guān)注我們。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容