从零开始用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 , 所有涉及到的资源文件和完整项目可以到 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 触摸板五指张开的手势隐藏了窗口才关掉了程序
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。