Electron桌面應用開發基礎

2023-06-09 18:00:37

Electron桌面應用開發

Electron技術架構

地址:快速入門 | Electron

  • Chromium 支援最新特性的瀏覽器
  • Node.js Javascript執行時,可實現檔案讀寫
  • Native APIS 提供統一的原生介面能力

環境搭建

  1. Node 安裝 (我的版本14.15.0)
  2. 專案初始化
    npm init -y
    // 安裝Electron 
    npm i --save-dev electron
    // 建立main.js 並在package.json中設定為入口
    "main":"main.js"
    // 建立index.html 用來書寫頁面內容
    
  3. 初始化程式碼

    package.json

    {
      "name": "myElectron",
      "version": "1.0.0",
      "description": "",
      "main": "main.js",
      "scripts": {
        "start": "nodemon --watch main.js --exec npm run build",
        "build": "electron ."
      },
      "keywords": [],
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "electron": "^24.4.0"
      },
      "dependencies": {
        "@electron/remote": "^2.0.9"
      }
    }
    

    main.js

    const { app, BrowserWindow } = require( 'electron' )
    // app 哪個控制應用程式的事件生命週期
    // BrowserWindow   建立和管理應用程式Windows 
    const createWindow = () => {
      let mainWin = new BrowserWindow( {
        width: 800,
        height: 600,
        show: false,
        backgroundColor: 'aqua',
    	} )
      mainWin.loadFile( 'index.html' )
      mainWin.on( 'ready-to-show', () => {
        mainWin.show()
      	} )
      mainWin.on( 'close', () => {
        mainWin = null
      	} )
    }
    app.on( 'ready', () => {
      createWindow()
    } )
    

生命週期事件 // 執行順序如下

  • ready: app初始化完成

  • dom-ready: 一個視窗中的文字載入完成

  • did-finsh-load: 導航完成時觸發

  • closed:當視窗關閉時觸發,此時應刪除視窗參照

  • window-all-closed: 所有視窗關閉時觸發

  • before-quit: 在關閉視窗之前觸發

  • will-quit: 在視窗關閉並且應用退出時觸發

  • quit: 當所有視窗被關閉時觸發

  • 
     mainWin.webContents.on( 'did-finish-load', () => {
        console.log( '333-did-finish-load' );
      } )
      // 視窗中文字載入完成
      mainWin.webContents.on( 'dom-ready', () => {
        console.log( '222dom-ready' );
      } )
      // 主視窗關閉
      mainWin.on( 'closed', () => {
        console.log( '88888-事件發生' );
      } )
    
    app.on( 'window-all-closed', () => {
      console.log( '444-window-all-closed' );
      if ( process.platform !== 'darwin' ) app.quit()
    } )
    app.on( 'ready', () => {
      console.log( '111- app初始化完成' );
      createWindow()
    } )
    
    app.on( 'before-quit', () => {
      console.log( '555-before-quit' );
    } )
    app.on( 'will-quit', () => {
      console.log( '666-will-quit' );
    } )
    app.on( 'quit', () => {
      console.log( '777-quit' );
    } )
    

視窗設定

const mainWin = new BrowserWindow( {
    x: 100,  //x y 視窗開始位置
    y: 100,   
    show: false,
    width: 800,
    height: 600,
    minHeight: 400, // min max 最小最大寬高
    minWidth: 50,
    maxHeight: 1000,
    maxWidth: 1200,
    resizable: false, // 視窗是否可調整
    minimizable: true,
    maximizable: true,
    title: '桌面應用',
    frame: false,
    // autoHideMenuBar: true,
    webPreferences: {
      nodeIntegration: true,  // 執行渲染程序使用node
      enableRemoteModule: true,
      contextIsolation: false
    }
  } )
  require( '@electron/remote/main' ).initialize()
  require( "@electron/remote/main" ).enable( mainWin.webContents )

視窗標題及環境

  • 標題設定

    1. 優先讀取index.html中的title
    2. index.html中不設定得情況下 可以讀取 new BrowserWindow 中設定的title
  • 圖示修改
    icon

  • frame 是否顯示預設導航選單 + 標題

  • transparent 設定透明

  • autoHideMenuBar 是否隱藏 導航選單

  • 點選開啟新視窗
    備註: 在main中執行時是主程序 在index.html中執行時是渲染程序
    app BrowserWindow 都屬於主程序得模組
    出於安全考慮 渲染程序中沒有辦法使用require 可在main中設定
    渲染程序不允許直接使用主程序模組 通過remote進行呼叫
    electron 12 之後就已經廢除了 remote
    // 替換為:
    const { BrowserWindow } = require('@electron/remote')

// 在主程序中:
require('@electron/remote/main').initialize()

自定義視窗實現

  1. 渲染程序中獲取 主程序視窗範例
    let mainWin = remote.getCurrentWindow()
  2. 獲取各個按鈕
    var btnGroup = document.getElementsByClassName( 'btnGroup' )[0].getElementsByTagName( 'button' )
  3. 是否最大判斷
    mainWin.isMaximized()
  4. 最大化
mainWin.maximize()
// 最大化還原
mainWin.restore()
  1. 最小化
mainWin.minimize()
 // <div class="btnGroup">
 //   <button>最小</button>
 //   <button>最大</button>
 //   <button>關閉</button>
 // </div>
 
 // 獲取按鈕組
  var btnGroup = document.getElementsByClassName( 'btnGroup' )[0].getElementsByTagName( 'button' )
  btnGroup[0].addEventListener( 'click', () => {
    if ( !mainWin.isMinimized() ) {
      mainWin.minimize()
    }
  } )
  btnGroup[1].addEventListener( 'click', () => {
    console.log( '最大化', mainWin.isMaximized() );
    if ( !mainWin.isMaximized() ) {  //  判斷視窗是否最大化
      mainWin.maximize()  // 最大化
    } else {
      mainWin.restore()
    }
  } )
  btnGroup[2].addEventListener( 'click', () => {
    mainWin.close()
  } )

阻止視窗關閉

window.onbeforeunload = function() {
return false
}

動態建立選單

  1. 準備模板
let menuArr = [{label:'開啟',type:'normal',role:'copy'}]
  1. 利用模板生成選單
let menu = Menu.buildFromTemplate( menuArr )
  1. 新增到應用
Menu.setApplicationMenu( menu )
let temp = [
  {
    label: 'send',
    click () {
      BrowserWindow.getFocusedWindow().webContents.send( 'msg2', '主程序發來訊息' )
    }
  }
]

let tem = Menu.buildFromTemplate( temp )
Menu.setApplicationMenu( tem )


// <button id="selfMenu">自定義選單</button>
  // <input type="text" value="" id="inputText">
  // <button id="addMenu">加入新增選單</button>

1. 找到  Menu  MenuItem
2. new Menu()  可以將  new MenuItem() 建立的選單進行新增到選單欄中

let remote = require( '@electron/remote' )
let Menu = remote.Menu
let MenuItem = remote.MenuItem
window.addEventListener( 'DOMContentLoaded', () => {
  // 獲取按鈕-- 自定義選單
  let selfBtn = document.getElementById( 'selfMenu' )
  let inputVal = document.getElementById( 'inputText' )
  let addMenu = document.getElementById( 'addMenu' )
  let selfMenuItem = new Menu()
  selfBtn.addEventListener( 'click', () => {
    let menuFile = new MenuItem( { label: '檔案', type: 'normal' } )
    let menuEdit = new MenuItem( { label: '編輯', type: 'normal' } )
    let menuSelf = new MenuItem( { label: '自定義選單', submenu: selfMenuItem } )
    let menu = new Menu()
    menu.append( menuFile )
    menu.append( menuEdit )
    menu.append( menuSelf )
    Menu.setApplicationMenu( menu )
  } )

  addMenu.addEventListener( 'click', () => {
    let content = inputVal.value.trim()
    if ( content ) {
      selfMenuItem.append( new MenuItem( { label: content, type: 'normal' } ) )
      content = ''
    }
  } )
})

右擊彈出選單

  1. 建立選單

  2. 監聽contextmenu 事件 並阻止預設行為

  3. menu.popup({window:remote.getCurrentWindow()})

     // 右鍵選單
      let rightMenu = [
        {
          label: 'Run Code',
          type: 'normal'
        },
        {
          label: '重新整理',
          role: 'refresh'
        },
        {
          type: 'separator'
        },
        {
          label: '其他功能',
          click () {
            console.log( '其他功能已執行' );
          }
        }
      ]
      let menuRight = Menu.buildFromTemplate( rightMenu )
      window.addEventListener( 'contextmenu', ( e ) => {
        e.preventDefault()
        menuRight.popup( {
          window: remote.getCurrentWindow
        } )
      } )
    

主程序與渲染程序通訊

  1. ipcRender(on send) ipcMain( on ) 兩個程序之間通訊
- ipcMain  內部  e.sender.send('xx',xxx)
- ipcMain  內部接收 e.returnValue
  1. BrowserWindow.getFocusedWindow().webContents.send('mtp',來自於主程序的訊息) // 依賴按鈕之類的事件觸發‘

  2. mainWin.contents.openDevtools() // 開啟控制檯

    // main.js
    let { app, BrowserWindow, ipcMain } = require( 'electron' )
    ipcMain.on( 'msg1', ( e, ev ) => {
      console.log( e, ev );
      e.sender.send( 'msg2', 666 )
      // BrowserWindow.getFocusedWindow().webContents.send( 'msg2', 666 )
    } )
    
    // index.js
    let remote = require( '@electron/remote' )
    let { ipcRenderer } = require( 'electron' )
    window.addEventListener( 'DOMContentLoaded', () => {
      console.log( 666 );
      ipcRenderer.send( 'msg1', '渲染程序發來賀電' )
      ipcRenderer.on( 'msg2', ( e, ev ) => {
        console.log( e, ev );
      } )
    } )
    
    

localtorage通訊

  1. 獲取主視窗id
- BrowserWindow  範例屬性  id
  1. 子視窗設定ID
- BrowserWindow.fromId(mainWin.id)
  1. 通訊的時候儲存 資訊到localStorage中

  2. 新視窗開啟時取值並使用

    // main.js
    ipcMain.on( 'msg', ( e, data ) => {
        if ( data ) {
          // 開啟第二個視窗
          let sub2 = new BrowserWindow( {
            parent: BrowserWindow.fromId( mainId ),
            width: 300,
            height: 150,
            webPreferences: {
              nodeIntegration: true,
              enableRemoteModule: true,
              contextIsolation: false
            }
          } )
          sub2.loadFile( 'sub.html' )
          sub2.on( 'close', () => {
            sub2 = null
          } )
        }
      } )
      // 主程序接收視窗二的資訊
      ipcMain.on( 'toMain', ( e, data ) => {
        // 將資料轉發給index程序
        let mainOpen = BrowserWindow.fromId( mainId )
        mainOpen.webContents.send( 'win2', data )
      } )
    
    // index.js
    let remote = require( '@electron/remote' )
    let { ipcRenderer } = require( 'electron' )
    
    window.addEventListener( 'DOMContentLoaded', () => {
    
      // 獲取開啟視窗按鈕
      let btnOpen = document.getElementById( 'openTwo' )
      btnOpen.addEventListener( 'click', () => {
        localStorage.setItem( 'name', '張三' )
        ipcRenderer.send( 'msg', true )
      } )
      ipcRenderer.on( 'win2', ( e, data ) => {
        console.log( 'index程序已經接收到', data );
      } )
    } )
    

渲染程序之間通訊

  1. 主程序 A 渲染 B渲染
  2. A給B發生資料 ipcRender.send
  3. B接收 ipcRender.on
  4. B給A發資料 先傳送給 主程序 主程序再傳送給 A
  5. 注意: 主程序傳送時不能直接使用BrowsserWindow.getFocusedWindow().webContents.send('mtp',來自於主程序的訊息)
    因為當前焦點視窗不一定時主程序視窗

dialog模組

  • dialog模組: 主程序模組 在渲染程序中使用remote.diaog 呼叫對應觸發得視窗api
  • 設定:
    remote.dialog.showOpenDialog( {
    defaultPath: __dirname,
    buttonLabel: '請選擇',
    title: '高傲的狼',
    properties: ['openFile', 'multiSelections'],
    filters: [
    { name: '程式碼檔案', extensions: ['js', 'html', 'json'] },
    { name: '圖片檔案', extensions: ['ico', 'jpeg', 'png'] },
    { name: '媒體檔案', extensions: ['mp3', 'mp4', 'avi'] },
    ]
    } ).then( res => {
    console.log( res );
    } )

shell 開啟url或者路勁

  1. shell.openExternal(url) 預設瀏覽器開啟
  2. shell.showItemInFolder() 在桌面參照中開啟

訊息提示

  • option = {
    title:'高傲的狼',
    body:'你好哇,小傢伙',
    icon:'./xxx.ico'
    }
  1. window.Notification(
option.title,option

)

快捷鍵

  1. 註冊
globalShortcut('ctrl + q')  // 返回布林值  true/false
  1. 判斷是否註冊過
globalShortcut.isRegistered('ctrl + q')
  1. 登出快捷鍵
- 時機 -- 在生命週期 will-quit中進行登出
- globalShortcut.unregister('ctrl + q')

剪下板模組

  1. clipboard 模組
- writeText
- readText
  1. 圖片複製 貼上
    1. let img = nativeImage.createFromPath( './女孩.jpg' )
      2. clipboard.writeImage( img )
// 將剪貼版中圖片寫到DOM中
3. let oimg = clipboard.readImage()
4. let imgDom = new Image()
5. imgDom.src = oimg.toDataURL()
6. document.body.appendChild( imgDom )