从零开始用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 相关文章推荐
让Firefox支持event对象实现代码
Nov 07 Javascript
代码获取历史上的今天发生的事
Apr 11 Javascript
JavaScript模块随意拖动示例代码
May 27 Javascript
JavaScript判断数组重复内容的两种方法(推荐)
Jun 06 Javascript
HTML中setCapture、releaseCapture 使用方法浅析
Sep 25 Javascript
JS中传递参数的几种不同方法比较
Jan 20 Javascript
js replace()去除代码中空格的实例
Feb 14 Javascript
原生js更改css样式的两种方式
Mar 15 Javascript
详解vee-validate的使用个人小结
Jun 07 Javascript
基于JavaScript实现前端数据多条件筛选功能
Aug 19 Javascript
jQuery的ztree仿windows文件新建和拖拽功能的实现代码
Dec 05 jQuery
JS+CSS+HTML实现“代码雨”类似黑客帝国文字下落效果
Mar 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 中dirname(_file_)讲解
2007/03/18 PHP
php分页示例代码
2007/03/19 PHP
PHP判断网络文件是否存在的方法
2015/03/12 PHP
php中html_entity_decode实现HTML实体转义
2018/06/13 PHP
php项目中类的自动加载实例讲解
2019/09/12 PHP
javascript Array对象基础知识小结
2010/11/16 Javascript
你可能不知道的JavaScript的new Function()方法
2014/04/17 Javascript
页面js遇到乱码问题的解决方法是和无法转码的情况
2014/04/30 Javascript
jquery实现Slide Out Navigation滑出式菜单效果代码
2015/09/07 Javascript
js文字横向滚动特效
2015/11/11 Javascript
详解使用grunt完成requirejs的合并压缩和js文件的版本控制
2017/03/02 Javascript
Vue.js实现输入框绑定的实例代码
2017/08/24 Javascript
nodejs 图片预览和上传的示例代码
2017/09/30 NodeJs
在vue项目中使用Nprogress.js进度条的方法
2018/01/31 Javascript
vue实现拖拽的简单案例 不超出可视区域
2019/07/25 Javascript
vue实现中部导航栏布局功能
2019/07/30 Javascript
Vue使用vue-recoure + http-proxy-middleware + vuex配合promise实现基本的跨域请求封装
2019/10/21 Javascript
jquery实现广告上下滚动效果
2021/03/04 jQuery
[01:01:18]DOTA2上海特级锦标赛主赛事日 - 2 败者组第二轮#2COL VS LGD
2016/03/03 DOTA
python字典键值对的添加和遍历方法
2016/09/11 Python
解决Ubuntu pip 安装 mysql-python包出错的问题
2018/06/11 Python
pycharm的python_stubs问题
2020/04/08 Python
使用jupyter notebook将文件保存为Markdown,HTML等文件格式
2020/04/14 Python
Python实现列表索引批量删除的5种方法
2020/11/16 Python
Python用SSH连接到网络设备
2021/02/18 Python
css3利用transform变形结合事件完成扇形导航
2020/10/26 HTML / CSS
CSS3 文字动画效果
2020/11/12 HTML / CSS
const char*, char const*, char*const的区别是什么
2014/07/09 面试题
库房主管岗位职责
2013/12/31 职场文书
新闻专业毕业生英文求职信
2014/03/19 职场文书
高中运动会广播稿
2014/09/16 职场文书
大明湖导游词
2015/02/03 职场文书
少先队工作总结2015
2015/05/13 职场文书
2016年社区六一儿童节活动总结
2016/04/06 职场文书
Redis延迟队列和分布式延迟队列的简答实现
2021/05/13 Redis
Redis高可用集群redis-cluster详解
2022/03/20 Redis