从零开始用electron手撸一个截屏工具的示例代码


Posted in Javascript onOctober 10, 2018

最近在尝试利用 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 即可看到一个窗口, 窗口中有一个按钮

从零开始用electron手撸一个截屏工具的示例代码

触发截屏

截屏是一个相对独立的功能, 并且有可能会有全局快捷键以及菜单触发等脱离窗口的情况, 所以截屏的触发应该放在 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 中下载

截图交互

从零开始用electron手撸一个截屏工具的示例代码

完成的功能有截取指定区域图片, 拖拽移动和改变选区尺寸, 实时尺寸显示和工具条

获取屏幕截图

// 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 触摸板五指张开的手势隐藏了窗口才关掉了程序

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javaScript 动态访问JSon元素示例代码
Aug 30 Javascript
jquery 中的each()跳出循环的语句
May 23 Javascript
使用Node.js实现一个简单的FastCGI服务器实例
Jun 09 Javascript
innerHTML动态添加html代码和脚本兼容多个浏览器
Oct 11 Javascript
使用jQuery仿苹果官网焦点图特效
Dec 23 Javascript
js实现的二级横向菜单条实例
Aug 22 Javascript
js中window.open的参数及注意注意事项
Jul 06 Javascript
Extjs 点击复选框在表格中增加相关信息行
Jul 12 Javascript
react系列从零开始_简单谈谈react
Jul 06 Javascript
JavaScript Dom 绑定事件操作实例详解
Oct 02 Javascript
vue excel上传预览和table内容下载到excel文件中
Dec 10 Javascript
JS面向对象之多选框实现
Jan 17 Javascript
js正则取值的结果数组调试方法
Oct 10 #Javascript
webpack dll打包重复问题优化的解决
Oct 10 #Javascript
4个顶级JavaScript高级文本编辑器
Oct 10 #Javascript
Koa代理Http请求的示例代码
Oct 10 #Javascript
解决js相同的正则多次调用test()返回的值却不同的问题
Oct 10 #Javascript
jQuery 获取除某指定对象外的其他对象 ( :not() 与.not())
Oct 10 #jQuery
微信小程序自定义组件的实现方法及自定义组件与页面间的数据传递问题
Oct 09 #Javascript
You might like
php生成缩略图的类代码
2008/10/02 PHP
fleaphp crud操作之find函数的使用方法
2011/04/23 PHP
反射调用private方法实践(php、java)
2015/12/21 PHP
Laravel基础_关于view共享数据的示例讲解
2019/10/14 PHP
PHP设计模式入门之迭代器模式原理与实现方法分析
2020/04/26 PHP
Jquery 基础学习笔记之文档处理
2009/05/29 Javascript
JS对外部文件的加载及对IFRMAME的加载的实现,当加载完成后,指定指向方法(方法回调)
2011/07/04 Javascript
JavaScript转换二进制编码为ASCII码的方法
2015/04/16 Javascript
避免jQuery名字冲突 noConflict()方法
2016/07/30 Javascript
ES6新特性之Symbol类型用法分析
2017/03/31 Javascript
JS实现闭包中的沙箱模式示例
2017/09/07 Javascript
基于datepicker定义自己的angular时间组件的示例
2018/03/14 Javascript
ant design实现圈选功能
2019/12/17 Javascript
vue 封装 Adminlte3组件的实现
2020/03/18 Javascript
Vue 构造选项 - 进阶使用说明
2020/08/14 Javascript
python中使用sys模板和logging模块获取行号和函数名的方法
2014/04/15 Python
python抓取网页时字符集转换问题处理方案分享
2014/06/19 Python
Python内置函数之filter map reduce介绍
2014/11/30 Python
python 实现求解字符串集的最长公共前缀方法
2018/07/20 Python
Pandas库之DataFrame使用的学习笔记
2019/06/21 Python
Python正则表达式匹配数字和小数的方法
2019/07/03 Python
TensorFLow 不同大小图片的TFrecords存取实例
2020/01/20 Python
解决PDF 转图片时丢文字的一种可能方式
2021/03/04 Python
2014年圣诞节倒计时网页的制作过程
2014/12/05 HTML / CSS
string = null 和string = ''的区别
2013/04/28 面试题
腾讯公司的一个sql题
2013/01/22 面试题
传播学毕业生求职信
2013/10/11 职场文书
自我鉴定三原则
2014/01/13 职场文书
五年级语文教学反思
2014/01/30 职场文书
乡镇党的群众路线教育实践活动总结报告
2014/10/30 职场文书
2014年公务员转正工作总结
2014/11/07 职场文书
社区活动总结范文
2015/05/07 职场文书
2015中学政教处工作总结
2015/07/22 职场文书
python如何利用cv2模块读取显示保存图片
2021/06/04 Python
自动在Windows中运行Python脚本并定时触发功能实现
2021/09/04 Python
基于Redis zSet实现滑动窗口对短信进行防刷限流的问题
2022/02/12 Redis