用electron開發了一個螢幕截圖工具

nashaofu發表於2019-03-04

前段時間做了一個釘釘的Linux版本,由於是基於網頁版做的,所以缺失了很多桌面應用程式的功能。由於使用的使用者多是Linux的使用者,所以在Linux的截圖功能沒有,在幾個使用者的要求下決定做一個截圖功能。

專案目前支援顯示器截圖,在windows上執行效果比較理想,Linux上有一定的BUG,目前還不能夠支援跨螢幕截圖(一個截圖橫跨兩個顯示器)功能,本文也釋出在了簡書,也可以去簡書閱讀,傳送門www.jianshu.com/p/276a29b28…

electron截圖API

在electron中提供了desktopCapturer模組,該模組只能在渲染程式使用。
該模組只提供了一個方法desktopCapturer.getSources(options, callback)

  • options是一個物件,其中包含兩個引數
    • types: 一個 String 陣列,列出了可以捕獲的桌面資源型別, 可用型別為 screen 和 window.
    • thumbnailSize (可選) :建議縮略可被縮放的 size, 預設為 {width: 150, height: 150}.
  • callback(error, sources)是一個回撥函式,其中會傳遞兩個引數:
    • error: 獲取截圖失敗時的錯誤資訊
    • sources: 是一個 Source 物件陣列, 每個 Source 表示了一個捕獲的螢幕或單獨視窗,並且有如下屬性
      • id: 在 navigator.webkitGetUserMedia中使用的捕獲視窗或螢幕的id,格式為 window:XX或者screen:XX,XX是一個隨機數
      • name:捕獲視窗或螢幕的描述名,如果資源為螢幕,名字為Entire Screen或Screen ; 如果資源為視窗, 名字為視窗的標題
      • thumbnail: 螢幕縮圖

螢幕截圖功能編寫

  1. 為了能夠符合大多數使用者的習慣,特別是用慣了QQ截圖功能的小夥伴,所以使用了快捷鍵ctrl+alt+a來截圖
  2. 所有的程式處理程式碼都必須等到app ready事件之後再處理,否則會報錯,所以所有程式碼都放到了ready事件的回撥函式中。
  3. 為了把截圖功能給獨立出來不與其他模組相互干擾,所以就把截圖相關的主程式程式碼單獨寫到檔案shortcut-capture.js,並把模組封裝為一個函式,並且通過變數控制,保證整個應用程式內只會執行一次初始化截圖模組。
  4. 主程式程式碼如下

     // 引入各個模組
     const {
       globalShortcut,
       ipcMain,
       BrowserWindow,
       clipboard,
       nativeImage
     } = require(`electron`)
    
     // 保證函式只執行一次
     let isRuned = false
     // 截圖時會出現截圖介面,如下就是儲存截圖視窗的陣列
     const $windows = []
     // 判斷是否為快捷鍵退出,其他的退出方式都不被允許
     let isClose = false
     module.exports = mainWindow => {
       if (isRuned) {
         return
       }
       isRuned = true
    
       // 註冊全域性快捷鍵
       globalShortcut.register(`ctrl+alt+a`, function () {
         mainWindow.webContents.send(`shortcut-capture`)
       })
    
       // 抓取截圖之後顯示視窗
       ipcMain.on(`shortcut-capture`, (e, sources) => {
         // 如果有以前的視窗就關閉以前的視窗
         // 然後根據截圖資源於螢幕資料生成視窗
         closeWindow()
         sources.forEach(source => {
           createWindow(source)
         })
       })
       // 有一個視窗關閉就關閉所有的視窗
       ipcMain.on(`cancel-shortcut-capture`, closeWindow)
    
       // 截圖視窗確認截圖時把資料傳遞到主程式
       // 然後把資料寫入到剪下板,並關閉視窗
       // 沒有直接在渲染程式把資料寫入剪下板是因為在Linux上會報錯
       // 所以就把這一步改到主程式完成
       ipcMain.on(`set-shortcut-capture`, (e, dataURL) => {
         clipboard.writeImage(nativeImage.createFromDataURL(dataURL))
         closeWindow()
       })
     }
    
     // 建立視窗
     function createWindow (source) {
       // display為螢幕相關資訊
       // 特別再多螢幕的時候要定位各個視窗到對應的螢幕
       const { display } = source
       const $win = new BrowserWindow({
         title: `截圖`,
         width: display.size.width,
         height: display.size.height,
         x: display.bounds.x,
         y: display.bounds.y,
         frame: false,
         show: false,
         transparent: true,
         resizable: false,
         alwaysOnTop: true,
         fullscreen: true,
         skipTaskbar: true,
         closable: true,
         minimizable: false,
         maximizable: false
       })
       // 全屏視窗
       setFullScreen($win, display)
       // 只能通過cancel-shortcut-capture的方式關閉視窗
       $win.on(`close`, e => {
         if (!isClose) {
           e.preventDefault()
         }
       })
       // 頁面初始化完成之後再顯示視窗
       // 並檢測是否有版本更新
       $win.once(`ready-to-show`, () => {
         $win.show()
         $win.focus()
         // 重新調整視窗位置和大小
         setFullScreen($win, display)
       })
    
       // 當頁面載入完成時通知截圖視窗開始程式的執行
       $win.webContents.on(`dom-ready`, () => {
         $win.webContents.executeJavaScript(`window.source = ${JSON.stringify(source)}`)
         $win.webContents.send(`dom-ready`)
         $win.focus()
       })
       // 載入地址
       $win.loadURL(`file://${__dirname}/window/shortcut-capture.html`)
       $windows.push($win)
     }
    
     // 讓視窗全屏
     function setFullScreen ($win, display) {
       $win.setBounds({
         width: display.size.width,
         height: display.size.height,
         x: display.bounds.x,
         y: display.bounds.y
       })
       $win.setAlwaysOnTop(true)
       $win.setFullScreen(true)
     }
    
     // 關閉視窗
     function closeWindow () {
       isClose = true
       while ($windows.length) {
         const $winItem = $windows.pop()
         $winItem.close()
       }
       isClose = false
     }複製程式碼
  5. 主程式與渲染程式通訊通過ipcMain模組完成,ipcMain通過監聽渲染程式傳過來的事件獲得渲染程式的資料,並且兩個程式通訊資料只能是簡單物件。主程式向渲染程式傳遞資料是通過webContents的send方法實現的,渲染程式通過ipcRender物件事件監聽實現,同是主程式也可以通過webContents.executeJavaScript方法以字串的方式向頁面注入js進行執行。
  6. 當程式執行之後,當使用者按下快捷鍵後,主視窗的渲染程式就開始截圖,截圖後就把資料傳到主程式,然後主程式建立新視窗,並把截圖資料傳遞到新建立的視窗中,然後等待使用者的截圖操作
     // 主程式捕獲到截圖快捷鍵就讓渲染程式截圖
     ipcRenderer.on(`shortcut-capture`, () => {
       // 獲取螢幕數量
       // screen為electron的模組
       const displays = screen.getAllDisplays()
       // 每個螢幕都截圖一個
       // desktopCapturer.getSources可以一次獲取所有桌面的截圖
       // 但由於thumbnailSize不一樣所以就採用了每個桌面尺寸都捕獲一張
       const getDesktopCapturer = displays.map((display, i) => {
         return new Promise((resolve, reject) => {
           desktopCapturer.getSources({
             types: [`screen`],
             thumbnailSize: display.size
           }, (error, sources) => {
             if (!error) {
               return resolve({
                 display,
                 thumbnail: sources[i].thumbnail.toDataURL()
               })
             }
             return reject(error)
           })
         })
       })
       Promise.all(getDesktopCapturer)
         .then(sources => {
           // 把資料傳遞到主程式
           ipcRenderer.send(`shortcut-capture`, sources)
         })
         .catch(error => console.log(error))
     })複製程式碼
  7. 在本專案就採用了webContents.executeJavaScript的方法向頁面傳遞了截圖資料的
  8. 渲染程式接收到主程式的dom-ready事件之後就開始繪製截圖介面,並把頁面拖拽擷取圖片功能初始化。當使用者按下ESC按鍵的時候就關閉截圖視窗退出截圖
  9. 圖片裁剪功能。圖片裁剪是利用了canvas來實現的。canvas可以根據一張圖片來繪製出圖形,然後利用canvas的api把繪製出來的圖片給獲取成為可用的圖片資源,然後提交給主程式。其中主要利用了canvas的ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)方法
    • 其中image為圖片資源
    • sx、sy為原始圖片資源要繪製的開始的位置
    • sWidth、sHeight為原始圖片資源要繪製大小
    • dx、dy為把圖片繪製到畫布的起始位置
    • dWidth、dHeight為把圖片在畫布上繪製的大小
    • 本專案中sx、sy、 sWidth、sHeight都為擷取的區域大小和區域相對於視窗左上角的座標位置,dx、dy都為0,表示從畫布的左上角開始繪製,dWidth、dHeight為擷取區域大小,如果dWidth、dHeight和sWidth、sHeight不相等就可以實現擷取區域的縮放,但本專案是1:1的
  10. 擷取玩圖片之後點選截圖工具欄的確定按鈕,然後就會從canvas讀取圖片資訊,然後轉換為dataURL傳到主程式,主程式就把圖片資料寫入到剪下板並關閉視窗
  11. 由於截圖視窗渲染程式的程式碼較多,這裡就不上了,可以在Github上檢視,下附整個截圖的流程關係
    示意圖.png
    示意圖.png

最後,如果有時間的話,可也在考慮可以把截圖這個功能單獨提取出來然後做成一個模組,能夠在其他electron專案中直接引用即可。寫得不好的地方請各位大佬包容,GitHub專案地址:github.com/nashaofu/di…

相關文章