從零開始用electron手?jǐn)]一個(gè)截屏工具的示例代碼
最近在嘗試?yán)?electron 將一個(gè) web 版的聊天工具包裝成一個(gè)桌面 APP。作為一個(gè)聊天工具,截屏可以說(shuō)是一個(gè)必備功能了。不過(guò)遺憾的是沒有找到很成熟的庫(kù)來(lái)用,也可能是打開方式不對(duì),總之呢沒看到現(xiàn)成的,于是就想從頭擼一個(gè)簡(jiǎn)單的截圖工具。下面就進(jìn)入正題吧!
思路
electron 提供了截取屏幕的 API,可以輕松的獲取每個(gè)屏幕(存在外接顯示器的情況)和每個(gè)窗口的圖像信息。
- 把圖片截取出來(lái),然后創(chuàng)建一個(gè)全屏的窗口蓋住整個(gè)屏幕,將截取的圖片繪制在窗口上,然后再覆蓋一層黑色半透明的元素,看起來(lái)就像屏幕定住了一樣;
- 在窗口上增加交互制作選區(qū)的效果;
- 點(diǎn)擊確定,利用 canvas 對(duì)應(yīng)選區(qū)的位置截取圖片內(nèi)容,寫入剪貼板和保存圖片。
搭建項(xiàng)目
首先創(chuàng)建 package.json
填寫項(xiàng)目的必要信息, 注意 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" } }
創(chuàng)建 main.js
, 代碼來(lái)自 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() { // 創(chuàng)建瀏覽器窗口。 win = new BrowserWindow({ width: 800, height: 600 }) // 然后加載應(yīng)用的 index.html。 win.loadFile('index.html') // 打開開發(fā)者工具 win.webContents.openDevTools() // 當(dāng) window 被關(guān)閉,這個(gè)事件會(huì)被觸發(fā)。 win.on('closed', () => { // 取消引用 window 對(duì)象,如果你的應(yīng)用支持多窗口的話, // 通常會(huì)把多個(gè) window 對(duì)象存放在一個(gè)數(shù)組里面, // 與此同時(shí),你應(yīng)該刪除相應(yīng)的元素。 win = null }) } // Electron 會(huì)在初始化后并準(zhǔn)備 // 創(chuàng)建瀏覽器窗口時(shí),調(diào)用這個(gè)函數(shù)。 // 部分 API 在 ready 事件觸發(fā)后才能使用。 app.on('ready', createWindow) // 當(dāng)全部窗口關(guān)閉時(shí)退出。 app.on('window-all-closed', () => { // 在 macOS 上,除非用戶用 Cmd + Q 確定地退出, // 否則絕大部分應(yīng)用及其菜單欄會(huì)保持激活。 if (process.platform !== 'darwin') { app.quit() } }) app.on('activate', () => { // 在macOS上,當(dāng)單擊dock圖標(biāo)并且沒有其他窗口打開時(shí), // 通常在應(yīng)用程序中重新創(chuàng)建一個(gè)窗口。 if (win === null) { createWindow() } })
創(chuàng)建 index.html
, html 中放了一個(gè)按鈕, 用來(lái)觸發(fā)截屏操作
<!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>
這樣一個(gè)簡(jiǎn)單的 electron 項(xiàng)目就完成了, 執(zhí)行 yarn start
或者 npm start
即可看到一個(gè)窗口, 窗口中有一個(gè)按鈕
觸發(fā)截屏
截屏是一個(gè)相對(duì)獨(dú)立的功能, 并且有可能會(huì)有全局快捷鍵以及菜單觸發(fā)等脫離窗口的情況, 所以截屏的觸發(fā)應(yīng)該放在 main 進(jìn)程中來(lái)實(shí)現(xiàn)
在 renderer 進(jìn)程中可以通過(guò) ipc 通訊來(lái)完成, 在頁(yè)面的代碼中使用 ipcRenderer 發(fā)送事件, 而在 main 中使用 ipcMain 接收事件
// index.html const { ipcRenderer } = require('electron') document.getElementById('js-capture').addEventListener('click', ()=>{ ipcRenderer.send('capture-screen') })
在 main 進(jìn)程中接收 capture-screen
事件
// main.js // 接收事件 ipcMain.on('capture-screen', captureScreen)
同時(shí)加入全局快捷鍵觸發(fā)和取消截屏
// main.js // 注冊(cè)全局快捷鍵 // globalShortcut 需要在 app ready 之后 globalShortcut.register('CmdOrCtrl+Shift+A', captureScreen) globalShortcut.register('Esc', () => { if (captureWin) { captureWin.close() captureWin = null } })
通過(guò)快捷鍵和事件來(lái)觸發(fā)截屏方法 captureScreen
, 接下來(lái)實(shí)現(xiàn)這個(gè)方法來(lái)創(chuàng)建一個(gè)截屏窗口
創(chuàng)建截屏窗口
截屏窗口是要?jiǎng)?chuàng)建一個(gè)全屏的窗口, 并且把屏幕圖片繪制在窗口上, 再通過(guò)鼠標(biāo)拖拽等交互操作選出特定區(qū)域的圖像.
第一步是要?jiǎng)?chuàng)建窗口
// 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 設(shè)置為 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')) // 調(diào)試用 // captureWin.openDevTools() captureWin.on('closed', () => { captureWin = null }) }
窗口需要覆蓋全屏, 并且完全置頂, 在 windows 下可以使用 fullscreen
來(lái)保證全屏, Mac 下 fullscreen 會(huì)把窗口移到單獨(dú)桌面, 所以采用了另外的辦法, 代碼注釋上標(biāo)注了不同系統(tǒng)的相關(guān)選項(xiàng), 具體內(nèi)容可以查看文檔
注意這里窗口加載了另外一個(gè) html 文件, 這個(gè)文件用來(lái)負(fù)責(zé)截屏和裁剪的一些交互工作
capture.html
首先 html 結(jié)構(gòu)
// 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 : 繪制選中的圖片區(qū)域和邊框 Size info : 標(biāo)識(shí)截取范圍的尺寸 Toolbar : 操作按鈕, 用來(lái)取消和保存等 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; }
各個(gè)元素基本為 absolute 定位, 由 js 控制位置 按鈕使用了 iconfont , 所有涉及到的資源文件和完整項(xiàng)目可以到 GitHub - chrisbing/electorn-capture-screen: electron capture screen 中下載
截圖交互
完成的功能有截取指定區(qū)域圖片, 拖拽移動(dòng)和改變選區(qū)尺寸, 實(shí)時(shí)尺寸顯示和工具條
獲取屏幕截圖
// 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()
可以獲取主屏幕的大小和縮放比例, 縮放比例在高分屏中適用, 在高分屏中屏幕的物理尺寸和窗口尺寸并不一致, 一般會(huì)有2倍3倍等縮放倍數(shù), 所以為了獲取到高清的屏幕截圖, 需要在屏幕尺寸基礎(chǔ)上乘以縮放倍數(shù)
desktopCapturer
獲取屏幕截圖的圖片信息, 獲取的是一個(gè)數(shù)組, 包含了每一個(gè)屏幕的信息, 這里呢暫時(shí)只處理了第一個(gè)屏幕的信息
獲取了截圖信息后創(chuàng)建 CaptureRenderer 進(jìn)行交互處理
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() { // ... } }
代碼有點(diǎn)長(zhǎng), 由于篇幅的原因, 這里只列出了關(guān)鍵部分, 完整代碼請(qǐng)到 GitHub - chrisbing/electorn-capture-screen: electron capture screen 上查看
初始化時(shí)保存一份繪制了全部圖片的 canvas , 用來(lái)后續(xù)取選區(qū)部分圖片用
繪制過(guò)程中從 通過(guò) canvas 中的 getImageData
獲取圖片內(nèi)容 然后通過(guò) putImageData
繪制到顯示 canvas 中
附加內(nèi)容
在 CaptureRenderer 類中處理了圖片的選取. 還需要工具條和尺寸信息
這一部分代碼和圖片選取關(guān)系不是很大, 所以在外部單獨(dú)處理, 通過(guò) 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' })
移動(dòng)過(guò)程中計(jì)算尺寸, 并且實(shí)時(shí)計(jì)算位置, 移動(dòng)過(guò)程中隱藏工具條
重置選區(qū)時(shí)隱藏工具條和尺寸標(biāo)識(shí)
保存剪貼板
// 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)
通過(guò) nativeImage.createFromDataURL
創(chuàng)建圖片寫入剪貼板, 通知 main 進(jìn)程截圖完畢, 并附帶圖片的 base64 url, 然后關(guān)閉窗口
保存到文件
// 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
選擇保存文件名, 然后通過(guò) fs 模塊寫入文件
最終整體目錄結(jié)構(gòu)
├── index.html ├── lib // 截圖核心代碼 │ ├── assets // font 和 聲音資源 │ ├── capture-main.js // main 中截圖部分代碼 │ ├── capture-renderer.js // 截圖交互代碼 │ └── capture.html // 截圖 html ├── main.js └── package.json
坑點(diǎn)總結(jié)
開發(fā)過(guò)程中主要遇到了幾個(gè)坑
首先全屏窗口,在 windows 和 Mac 上存在不同處理,而且 mac 上這個(gè)方案在網(wǎng)上沒有查到,最后翻閱文檔無(wú)意中發(fā)現(xiàn)的
然后就是選區(qū)過(guò)程中,各個(gè)位置,選區(qū)的拖拽操作,需要大量時(shí)間調(diào)試
再有就是開發(fā)過(guò)程中代碼可能出錯(cuò),導(dǎo)致全屏窗口蓋在屏幕上無(wú)法去掉,最后通過(guò) mac 觸摸板五指張開的手勢(shì)隱藏了窗口才關(guān)掉了程序
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 使用electron制作滿屏心特效的示例代碼
- electron制作仿制qq聊天界面的示例代碼
- electron + vue項(xiàng)目實(shí)現(xiàn)打印小票功能及實(shí)現(xiàn)代碼
- electron中使用bootstrap的示例代碼
- Electron中實(shí)現(xiàn)大文件上傳和斷點(diǎn)續(xù)傳功能
- 使用electron將vue-cli項(xiàng)目打包成exe的方法
- 解決npm安裝Electron緩慢網(wǎng)絡(luò)超時(shí)導(dǎo)致失敗的問(wèn)題
- 詳解Webpack實(shí)戰(zhàn)之構(gòu)建 Electron 應(yīng)用
- 詳解Angular CLI + Electron 開發(fā)環(huán)境搭建
- 關(guān)于node-bindings無(wú)法在Electron中使用的解決辦法
相關(guān)文章
使用JavaScript截取視頻特定幀的實(shí)現(xiàn)方法
在網(wǎng)頁(yè)開發(fā)中,我們經(jīng)常需要對(duì)媒體文件進(jìn)行處理,其中包括視頻文件,有時(shí)候,我們可能需要從視頻中提取特定的幀,并將其顯示在網(wǎng)頁(yè)上,本文將介紹如何使用JavaScript來(lái)實(shí)現(xiàn)這一功能,感興趣的朋友跟著小編一起來(lái)看看吧2024-05-05JS針對(duì)瀏覽器窗口關(guān)閉事件的監(jiān)聽方法集錦
這篇文章主要介紹了JS針對(duì)瀏覽器窗口關(guān)閉事件的監(jiān)聽方法,總結(jié)整理了幾種常用的瀏覽器關(guān)閉事件監(jiān)聽方法,非常簡(jiǎn)單實(shí)用,需要的朋友可以參考下2016-06-06第九篇Bootstrap導(dǎo)航菜單創(chuàng)建步驟詳解
這篇文章主要介紹了Bootstrap導(dǎo)航菜單創(chuàng)建步驟詳解的相關(guān)資料,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-06-06Javascript isArray 數(shù)組類型檢測(cè)函數(shù)
在日常開發(fā)中,我們經(jīng)常需要判斷某個(gè)對(duì)象是否是數(shù)組類型的,在js中檢測(cè)對(duì)象類型的常見的方法有幾種.2009-10-10JavaScript實(shí)現(xiàn)簡(jiǎn)潔的俄羅斯方塊完整實(shí)例
這篇文章主要介紹了JavaScript實(shí)現(xiàn)簡(jiǎn)潔的俄羅斯方塊,以完整實(shí)例形式分析了JavaScript實(shí)現(xiàn)俄羅斯方塊游戲的具體技巧,代碼備有詳盡的注釋便于理解,需要的朋友可以參考下2016-03-03小程序?qū)崿F(xiàn)頁(yè)面頂部選項(xiàng)卡效果
這篇文章主要為大家詳細(xì)介紹了小程序?qū)崿F(xiàn)頁(yè)面頂部選項(xiàng)卡效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-11-11javascript完美實(shí)現(xiàn)給定日期返回上月日期的方法
這篇文章主要介紹了javascript完美實(shí)現(xiàn)給定日期返回上月日期的方法,結(jié)合實(shí)例形式分析了javascript日期時(shí)間的計(jì)算技巧,并給出了格式化日期時(shí)間的操作方法,需要的朋友可以參考下2017-06-06JS實(shí)現(xiàn)鼠標(biāo)移上去顯示圖片或微信二維碼
本文給大家分享一段使用的js代碼實(shí)現(xiàn)鼠標(biāo)移入顯示圖片或微信二維碼樣式,代碼簡(jiǎn)單易懂,非常不錯(cuò),需要的朋友參考下吧2016-12-12小程序?qū)崿F(xiàn)自定義導(dǎo)航欄適配完美版
這篇文章主要介紹了小程序?qū)崿F(xiàn)自定義導(dǎo)航欄適配完美版,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04