从零开始用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 相关文章推荐
jQuery+ajax实现顶一下,踩一下效果
Jul 17 Javascript
基于jQuery实现模拟页面加载进度条
Apr 01 Javascript
jquery和雅虎的yql服务实现天气预报服务示例
Feb 08 Javascript
jquery阻止后续事件只执行第一个事件
Jul 24 Javascript
编程语言JavaScript简介
Oct 16 Javascript
常用DOM整理
Jun 16 Javascript
javascript cookie的简单应用
Feb 24 Javascript
JavaScript中的Number数字类型学习笔记
May 26 Javascript
Mint-UI时间组件起始时间问题及时间插件使用
Aug 20 Javascript
vue 弹框产生的滚动穿透问题的解决
Sep 21 Javascript
leaflet加载geojson叠加显示功能代码
Feb 21 Javascript
使用jquery实现轮播图效果
Jan 02 jQuery
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 安全检测代码片段(分享)
2013/07/05 PHP
使用YII2框架实现微信公众号中表单提交功能
2017/09/04 PHP
ThinkPHP5+Layui实现图片上传加预览功能
2018/08/17 PHP
php解压缩zip和rar压缩包文件的方法
2019/07/10 PHP
如何用javascript判断录入的日期是否合法
2007/01/08 Javascript
jQuery动态添加的元素绑定事件处理函数代码
2011/08/02 Javascript
Package.js  现代化的JavaScript项目make工具
2012/05/23 Javascript
仿当当网淘宝网等主流电子商务网站商品分类导航菜单
2013/09/25 Javascript
Js控制滑轮左右滑动实例
2015/02/13 Javascript
实践中学习AngularJS表单
2016/03/21 Javascript
Bootstrap 粘页脚效果
2016/03/28 Javascript
如何使用headjs来管理和异步加载js
2016/11/29 Javascript
JS实现瀑布流布局
2017/10/21 Javascript
详解CommonJS和ES6模块循环加载处理的区别
2018/12/26 Javascript
JS实现选项卡效果的代码实例
2019/05/20 Javascript
微信小程序项目总结之记账小程序功能的实现(包括后端)
2019/08/20 Javascript
小程序自定义模板实现吸顶功能
2020/01/08 Javascript
js实现轮播图效果 z-index实现轮播图
2020/01/17 Javascript
vue+canvas实现拼图小游戏
2020/09/18 Javascript
[02:11]2014DOTA2 TI专访VG战队Fenrir:队伍气氛良好
2014/07/11 DOTA
python检测服务器是否正常
2014/02/16 Python
Python中的sort()方法使用基础教程
2017/01/08 Python
python算法与数据结构之单链表的实现代码
2019/06/27 Python
python多线程同步之文件读写控制
2021/02/25 Python
浅析python函数式编程
2020/09/26 Python
图解CSS3制作圆环形进度条的实例教程
2016/05/26 HTML / CSS
Dyson加拿大官方网站:购买戴森吸尘器,风扇,冷热器及配件
2016/10/26 全球购物
Brookstone美国官网:独特新奇产品
2017/03/04 全球购物
New Balance德国官方网站:购买鞋子和服装
2019/08/31 全球购物
公务员个人自我评价分享
2013/11/06 职场文书
2014年综治宣传月活动总结
2014/04/28 职场文书
刑事上诉状(无罪)
2015/05/23 职场文书
《雪地里的小画家》教学反思
2016/02/16 职场文书
导游词之台湾阿里山
2019/10/23 职场文书
Python数据结构之队列详解
2022/03/21 Python
20180830晚上第一届KSL半决赛 雨神vs解冻(二龙 三炮解说)
2022/04/01 星际争霸