最近在嘗試利用 electron 將一個 web 版的聊天工具包裝成一個桌面 APP。作為一個聊天工具,截屏可以說是一個必備功能了。不過遺憾的是沒有找到很成熟的庫來用,也可能是打開方式不對,總之呢沒看到現成的,于是就想從頭擼一個簡單的截圖工具。下面就進入正題吧!
思路
electron 提供了截取屏幕的 API,可以輕松的獲取每個屏幕(存在外接顯示器的情況)和每個窗口的圖像信息。
搭建項目
首先創建 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 winfunction 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.jslet captureWin = nullconst 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 , 所有涉及到的資源文件和完整項目可以到 GitHub - chrisbing/electorn-capture-screen: electron capture screen 中下載
截圖交互

完成的功能有截取指定區域圖片, 拖拽移動和改變選區尺寸, 實時尺寸顯示和工具條
獲取屏幕截圖
// capture-renderer.jsconst { 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.jsclass 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.jslet 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.jsconst 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 觸摸板五指張開的手勢隱藏了窗口才關掉了程序
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。
新聞熱點
疑難解答