從零開始用 electron 手擼一個截圖工具
最近在嘗試利用 electron 將一個 web 版的聊天工具包裝成一個桌面 APP。作為一個聊天工具,截圖可以說是一個必備功能了。不過遺憾的是沒有找到很成熟的庫來用,也可能是開啟方式不對,總之呢沒看到現成的,於是就想從頭擼一個簡單的截圖工具。下面就進入正題吧!
思路
electron 提供了擷取螢幕的 API,可以輕鬆的獲取每個螢幕(存在外接顯示器的情況)和每個視窗的影象資訊。
- 把圖片截取出來,然後建立一個全屏的視窗蓋住整個螢幕,將擷取的圖片繪製在視窗上,然後再覆蓋一層黑色半透明的元素,看起來就像螢幕定住了一樣;
- 在視窗上增加交互制作選區的效果;
- 點選確定,利用 canvas 對應選區的位置擷取圖片內容,寫入剪貼簿和儲存圖片。
搭建專案
首先建立 package.json
填寫專案的必要資訊, 注意 main 為入口檔案。
{ "name": "electorn-capture-screen", "version": "1.0.0", "main": "main.js", "repository": "https://github.com/chrisbing/electorn-capture-screen.git", "author": "Chris", "license": "MIT", "scripts": { "start": "electron ." }, "dependencies": { "electron": "^3.0.2" } } 複製程式碼
建立 main.js
, 程式碼來自 electron 官方文件
const { app, BrowserWindow, ipcMain, globalShortcut } = require('electron') const os = require('os') // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let win function createWindow() { // 建立瀏覽器視窗。 win = new BrowserWindow({ width: 800, height: 600 }) // 然後載入應用的 index.html。 win.loadFile('index.html') // 開啟開發者工具 win.webContents.openDevTools() // 當 window 被關閉,這個事件會被觸發。 win.on('closed', () => { // 取消引用 window 物件,如果你的應用支援多視窗的話, // 通常會把多個 window 物件存放在一個數組裡面, // 與此同時,你應該刪除相應的元素。 win = null }) } // Electron 會在初始化後並準備 // 建立瀏覽器視窗時,呼叫這個函式。 // 部分 API 在 ready 事件觸發後才能使用。 app.on('ready', createWindow) // 當全部視窗關閉時退出。 app.on('window-all-closed', () => { // 在 macOS 上,除非使用者用 Cmd + Q 確定地退出, // 否則絕大部分應用及其選單欄會保持啟用。 if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { // 在macOS上,當單擊dock圖示並且沒有其他視窗開啟時, // 通常在應用程式中重新建立一個視窗。 if (win === null) { createWindow() } }) 複製程式碼
建立 index.html
, html 中放了一個按鈕, 用來觸發截圖操作
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Hello World!</title> </head> <body> <button id="js-capture">Capture Screen</button> <script> const { ipcRenderer } = require('electron') document.getElementById('js-capture').addEventListener('click', ()=>{ ipcRenderer.send('capture-screen') }) </script> </body> </html> 複製程式碼
這樣一個簡單的 electron 專案就完成了, 執行 yarn start
或者 npm start
即可看到一個視窗, 視窗中有一個按鈕

觸發截圖
截圖是一個相對獨立的功能, 並且有可能會有全域性快捷鍵以及選單觸發等脫離視窗的情況, 所以截圖的觸發應該放在 main 程序中來實現
在 renderer 程序中可以通過 ipc 通訊來完成, 在頁面的程式碼中使用 ipcRenderer 傳送事件, 而在 main 中使用 ipcMain 接收事件
// index.html const { ipcRenderer } = require('electron') document.getElementById('js-capture').addEventListener('click', ()=>{ ipcRenderer.send('capture-screen') }) 複製程式碼
在 main 程序中接收 capture-screen
事件
// main.js // 接收事件 ipcMain.on('capture-screen', captureScreen) 複製程式碼
同時加入全域性快捷鍵觸發和取消截圖
// main.js // 註冊全域性快捷鍵 // globalShortcut 需要在 app ready 之後 globalShortcut.register('CmdOrCtrl+Shift+A', captureScreen) globalShortcut.register('Esc', () => { if (captureWin) { captureWin.close() captureWin = null } }) 複製程式碼
通過快捷鍵和事件來觸發截圖方法 captureScreen
, 接下來實現這個方法來建立一個截圖視窗
建立截圖視窗
截圖視窗是要建立一個全屏的視窗, 並且把螢幕圖片繪製在視窗上, 再通過滑鼠拖拽等互動操作選出特定區域的影象.
第一步是要建立視窗
// main.js let captureWin = null const captureScreen = (e, args) => { if (captureWin) { return } const { screen } = require('electron') let { width, height } = screen.getPrimaryDisplay().bounds captureWin = new BrowserWindow({ // window 使用 fullscreen,mac 設定為 undefined, 不可為 false fullscreen: os.platform() === 'win32' || undefined, // win width, height, x: 0, y: 0, transparent: true, frame: false, skipTaskbar: true, autoHideMenuBar: true, movable: false, resizable: false, enableLargerThanScreen: true, // mac hasShadow: false, }) captureWin.setAlwaysOnTop(true, 'screen-saver') // mac captureWin.setVisibleOnAllWorkspaces(true) // mac captureWin.setFullScreenable(false) // mac captureWin.loadFile(path.join(__dirname, 'capture.html')) // 除錯用 // captureWin.openDevTools() captureWin.on('closed', () => { captureWin = null }) } 複製程式碼
視窗需要覆蓋全屏, 並且完全置頂, 在 windows 下可以使用 fullscreen
來保證全屏, Mac 下 fullscreen 會把視窗移到單獨桌面, 所以採用了另外的辦法, 程式碼註釋上標註了不同系統的相關選項, 具體內容可以檢視文件
注意這裡視窗載入了另外一個 html 檔案, 這個檔案用來負責截圖和裁剪的一些互動工作
capture.html
首先 html 結構
// capture.html <div id="js-bg" class="bg"></div> <div id="js-mask" class="mask"></div> <canvas id="js-canvas" class="image-canvas"></canvas> <div id="js-size-info" class="size-info"></div> <div id="js-toolbar" class="toolbar"> <div class="iconfont icon-zhongzhi" id="js-tool-reset"></div> <div class="iconfont icon-xiazai" id="js-tool-save"></div> <div class="iconfont icon-guanbi" id="js-tool-close"></div> <div class="iconfont icon-duihao" id="js-tool-ok"></div> </div> <script src="capture-renderer.js"></script> 複製程式碼
Bg : 截圖圖片 Mask : 一層灰色遮罩 Canvas : 繪製選中的圖片區域和邊框 Size info : 標識擷取範圍的尺寸 Toolbar : 操作按鈕, 用來取消和儲存等 capture-renderer.js : js 程式碼
@import "./assets/iconfont/iconfont.css"; html, body, div { margin: 0; padding: 0; box-sizing: border-box; } .mask { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.6); } .bg { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } .image-canvas { position: absolute; display: none; z-index: 1; } .size-info { position: absolute; color: #ffffff; font-size: 12px; background: rgba(40, 40, 40, 0.8); padding: 5px 10px; border-radius: 2px; font-family: Arial Consolas sans-serif; display: none; z-index: 2; } .toolbar { position: absolute; color: #343434; font-size: 12px; background: #f5f5f5; padding: 5px 10px; border-radius: 4px; font-family: Arial Consolas sans-serif; display: none; box-shadow: 0 0 20px rgba(0, 0, 0, 0.4); z-index: 2; align-items: center; } .toolbar .iconfont { font-size: 24px; padding: 2px 5px; } 複製程式碼
各個元素基本為 absolute 定位, 由 js 控制位置 按鈕使用了 iconfont , 所有涉及到的資原始檔和完整專案可以到 ofollow,noindex">GitHub - chrisbing/electorn-capture-screen: electron capture screen 中下載
截圖互動

完成的功能有擷取指定區域圖片, 拖拽移動和改變選區尺寸, 實時尺寸顯示和工具條
獲取螢幕截圖
// capture-renderer.js const { ipcRenderer, clipboard, nativeImage, remote, desktopCapturer, screen } = require('electron') const Event = require('events') const fs = require('fs') const { bounds: { width, height }, scaleFactor } = screen.getPrimaryDisplay() const $canvas = document.getElementById('js-canvas') const $bg = document.getElementById('js-bg') const $sizeInfo = document.getElementById('js-size-info') const $toolbar = document.getElementById('js-toolbar') const $btnClose = document.getElementById('js-tool-close') const $btnOk = document.getElementById('js-tool-ok') const $btnSave = document.getElementById('js-tool-save') const $btnReset = document.getElementById('js-tool-reset') console.time('capture') desktopCapturer.getSources({ types: ['screen'], thumbnailSize: { width: width * scaleFactor, height: height * scaleFactor, } }, (error, sources) => { console.timeEnd('capture') let imgSrc = sources[0].thumbnail.toDataURL() let capture = new CaptureRenderer($canvas, $bg, imgSrc, scaleFactor) }) 複製程式碼
screen.getPrimaryDisplay()
可以獲取主螢幕的大小和縮放比例, 縮放比例在高分屏中適用, 在高分屏中螢幕的物理尺寸和視窗尺寸並不一致, 一般會有2倍3倍等縮放倍數, 所以為了獲取到高清的螢幕截圖, 需要在螢幕尺寸基礎上乘以縮放倍數
desktopCapturer
獲取螢幕截圖的圖片資訊, 獲取的是一個數組, 包含了每一個螢幕的資訊, 這裡呢暫時只處理了第一個螢幕的資訊
獲取了截圖資訊後建立 CaptureRenderer 進行互動處理
CaptureRenderer
// capture-renderer.js class CaptureRenderer extends Event { constructor($canvas, $bg, imageSrc, scaleFactor) { super() // ... this.init().then(() => { console.log('init') }) } async init() { this.$bg.style.backgroundImage = `url(${this.imageSrc})` this.$bg.style.backgroundSize = `${width}px ${height}px` let canvas = document.createElement('canvas') let ctx = canvas.getContext('2d') let img = await new Promise(resolve => { let img = new Image() img.src = this.imageSrc if (img.complete) { resolve(img) } else { img.onload = () => resolve(img) } }) canvas.width = img.width canvas.height = img.height ctx.drawImage(img, 0, 0) this.bgCtx = ctx // ... } // ... onMouseDrag(e) { // ... this.selectRect = {x, y, w, h, r, b} this.drawRect() this.emit('dragging', this.selectRect) // ... } drawRect() { if (!this.selectRect) { this.$canvas.style.display = 'none' return } const { x, y, w, h } = this.selectRect const scaleFactor = this.scaleFactor let margin = 7 let radius = 5 this.$canvas.style.left = `${x - margin}px` this.$canvas.style.top = `${y - margin}px` this.$canvas.style.width = `${w + margin * 2}px` this.$canvas.style.height = `${h + margin * 2}px` this.$canvas.style.display = 'block' this.$canvas.width = (w + margin * 2) * scaleFactor this.$canvas.height = (h + margin * 2) * scaleFactor if (w && h) { let imageData = this.bgCtx.getImageData(x * scaleFactor, y * scaleFactor, w * scaleFactor, h * scaleFactor) this.ctx.putImageData(imageData, margin * scaleFactor, margin * scaleFactor) } this.ctx.fillStyle = '#ffffff' this.ctx.strokeStyle = '#67bade' this.ctx.lineWidth = 2 * this.scaleFactor this.ctx.strokeRect(margin * scaleFactor, margin * scaleFactor, w * scaleFactor, h * scaleFactor) this.drawAnchors(w, h, margin, scaleFactor, radius) } drawAnchors(w, h, margin, scaleFactor, radius) { // ... } onMouseMove(e) { // ... document.body.style.cursor = 'move' // ... } onMouseUp(e) { this.emit('end-dragging') this.drawRect() } getImageUrl() { const { x, y, w, h } = this.selectRect if (w && h) { let imageData = this.bgCtx.getImageData(x * scaleFactor, y * scaleFactor, w * scaleFactor, h * scaleFactor) let canvas = document.createElement('canvas') let ctx = canvas.getContext('2d') ctx.putImageData(imageData, 0, 0) return canvas.toDataURL() } return '' } reset() { // ... } } 複製程式碼
程式碼有點長, 由於篇幅的原因, 這裡只列出了關鍵部分, 完整程式碼請到 GitHub - chrisbing/electorn-capture-screen: electron capture screen 上檢視
初始化時儲存一份繪製了全部圖片的 canvas , 用來後續取選區部分圖片用
繪製過程中從 通過 canvas 中的 getImageData
獲取圖片內容 然後通過 putImageData
繪製到顯示 canvas 中
附加內容
在 CaptureRenderer 類中處理了圖片的選取. 還需要工具條和尺寸資訊
這一部分程式碼和圖片選取關係不是很大, 所以在外部單獨處理, 通過 CaptureRenderer 傳出的事件和一些屬性即可完成互動
// capture-renderer.js let onDrag = (selectRect) => { $toolbar.style.display = 'none' $sizeInfo.style.display = 'block' $sizeInfo.innerText = `${selectRect.w} * ${selectRect.h}` if (selectRect.y > 35) { $sizeInfo.style.top = `${selectRect.y - 30}px` } else { $sizeInfo.style.top = `${selectRect.y + 10}px` } $sizeInfo.style.left = `${selectRect.x}px` } capture.on('start-dragging', onDrag) capture.on('dragging', onDrag) let onDragEnd = () => { if (capture.selectRect) { const { x, r, b, y } = capture.selectRect $toolbar.style.display = 'flex' $toolbar.style.top = `${b + 15}px` $toolbar.style.right = `${window.screen.width - r}px` } } capture.on('end-dragging', onDragEnd) capture.on('reset', () => { $toolbar.style.display = 'none' $sizeInfo.style.display = 'none' }) 複製程式碼
移動過程中計算尺寸, 並且實時計算位置, 移動過程中隱藏工具條
重置選區時隱藏工具條和尺寸標識
儲存剪貼簿
// capture-renderer.js const audio = new Audio() audio.src = './assets/audio/capture.mp3' let selectCapture = () => { if (!capture.selectRect) { return } let url = capture.getImageUrl() remote.getCurrentWindow().hide() audio.play() audio.onended = () => { window.close() } clipboard.writeImage(nativeImage.createFromDataURL(url)) ipcRenderer.send('capture-screen', { type: 'complete', url, }) } $btnOk.addEventListener('click', selectCapture) 複製程式碼
通過 nativeImage.createFromDataURL
建立圖片寫入剪貼簿, 通知 main 程序截圖完畢, 並附帶圖片的 base64 url, 然後關閉視窗
儲存到檔案
// capture-renderer.js $btnSave.addEventListener(‘click’, () => { let url = capture.getImageUrl() remote.getCurrentWindow().hide() remote.dialog.showSaveDialog({ filters: [{ name: ‘Images’, extensions: [‘png’, ‘jpg’, ‘gif’] }] }, function (path) { if (path) { fs.writeFile(path, new Buffer(url.replace(‘data:image/png;base64,’, ‘’), ‘base64’), function () { ipcRenderer.send(‘capture-screen’, { type: ‘complete’, url, path, }) window.close() }) } else { ipcRenderer.send(‘capture-screen’, { type: ‘cancel’, url, }) window.close() } }) }) 複製程式碼
利用 remote.dialog.showSaveDialog
選擇儲存檔名, 然後通過 fs 模組寫入檔案
最終整體目錄結構
├── index.html ├── lib // 截圖核心程式碼 │├── assets // font 和 聲音資源 │├── capture-main.js // main 中截圖部分程式碼 │├── capture-renderer.js// 截圖互動程式碼 │└── capture.html // 截圖 html ├── main.js └── package.json 複製程式碼
坑點總結
開發過程中主要遇到了幾個坑
首先全屏視窗,在 windows 和 Mac 上存在不同處理,而且 mac 上這個方案在網上沒有查到,最後翻閱文件無意中發現的
然後就是選區過程中,各個位置,選區的拖拽操作,需要大量時間除錯
再有就是開發過程中程式碼可能出錯,導致全屏視窗蓋在螢幕上無法去掉,最後通過 mac 觸控板五指張開的手勢隱藏了窗口才關掉了程式,:joy: