前段时间做了一个钉钉的Linux版本,由于是基于网页版做的,所以缺失了很多桌面应用程序的功能。由于使用的用户多是Linux的用户,所以在Linux的截图功能没有,在几个用户的要求下决定做一个截图功能。
项目目前支持显示器截图,在windows上运行效果比较理想,Linux上有一定的BUG,目前还不能够支持跨屏幕截图(一个截图横跨两个显示器)功能,本文也发布在了简书,也可以去简书阅读,传送门www.jianshu.com/p/276a29b28…
electron截图API
在electron中提供了desktopCapturer模块,该模块只能在渲染进程使用。
该模块只提供了一个方法desktopCapturer.getSources(options, callback)
:
- options是一个对象,其中包含两个参数
- types: 一个 String 数组,列出了可以捕获的桌面资源类型, 可用类型为 screen 和 window.
- thumbnailSize (可选) :建议缩略可被缩放的 size, 默认为 {width: 150, height: 150}.
- callback(error, sources)是一个回调函数,其中会传递两个参数:
- error: 获取截图失败时的错误信息
- sources: 是一个 Source 对象数组, 每个 Source 表示了一个捕获的屏幕或单独窗口,并且有如下属性
- id: 在 navigator.webkitGetUserMedia中使用的捕获窗口或屏幕的id,格式为 window:XX或者screen:XX,XX是一个随机数
- name:捕获窗口或屏幕的描述名,如果资源为屏幕,名字为Entire Screen或Screen ; 如果资源为窗口, 名字为窗口的标题
- thumbnail: 屏幕缩略图
屏幕截图功能编写
- 为了能够符合大多数用户的习惯,特别是用惯了QQ截图功能的小伙伴,所以使用了快捷键
ctrl+alt+a
来截图 - 所有的程序处理代码都必须等到app ready事件之后再处理,否则会报错,所以所有代码都放到了ready事件的回调函数中。
- 为了把截图功能给独立出来不与其他模块相互干扰,所以就把截图相关的主进程代码单独写到文件shortcut-capture.js,并把模块封装为一个函数,并且通过变量控制,保证整个应用进程内只会执行一次初始化截图模块。
主进程代码如下
// 引入各个模块 const { globalShortcut, ipcMain, BrowserWindow, clipboard, nativeImage } = require('electron') // 保证函数只执行一次 let isRuned = false // 截图时会出现截图界面,如下就是保存截图窗口的数组 const $windows = [] // 判断是否为快捷键退出,其他的退出方式都不被允许 let isClose = false module.exports = mainWindow => { if (isRuned) { return } isRuned = true // 注册全局快捷键 globalShortcut.register('ctrl+alt+a', function () { mainWindow.webContents.send('shortcut-capture') }) // 抓取截图之后显示窗口 ipcMain.on('shortcut-capture', (e, sources) => { // 如果有以前的窗口就关闭以前的窗口 // 然后根据截图资源于屏幕数据生成窗口 closeWindow() sources.forEach(source => { createWindow(source) }) }) // 有一个窗口关闭就关闭所有的窗口 ipcMain.on('cancel-shortcut-capture', closeWindow) // 截图窗口确认截图时把数据传递到主进程 // 然后把数据写入到剪切板,并关闭窗口 // 没有直接在渲染进程把数据写入剪切板是因为在Linux上会报错 // 所以就把这一步改到主进程完成 ipcMain.on('set-shortcut-capture', (e, dataURL) => { clipboard.writeImage(nativeImage.createFromDataURL(dataURL)) closeWindow() }) } // 创建窗口 function createWindow (source) { // display为屏幕相关信息 // 特别再多屏幕的时候要定位各个窗口到对应的屏幕 const { display } = source const $win = new BrowserWindow({ title: '截图', width: display.size.width, height: display.size.height, x: display.bounds.x, y: display.bounds.y, frame: false, show: false, transparent: true, resizable: false, alwaysOnTop: true, fullscreen: true, skipTaskbar: true, closable: true, minimizable: false, maximizable: false }) // 全屏窗口 setFullScreen($win, display) // 只能通过cancel-shortcut-capture的方式关闭窗口 $win.on('close', e => { if (!isClose) { e.preventDefault() } }) // 页面初始化完成之后再显示窗口 // 并检测是否有版本更新 $win.once('ready-to-show', () => { $win.show() $win.focus() // 重新调整窗口位置和大小 setFullScreen($win, display) }) // 当页面加载完成时通知截图窗口开始程序的执行 $win.webContents.on('dom-ready', () => { $win.webContents.executeJavaScript(`window.source = ${JSON.stringify(source)}`) $win.webContents.send('dom-ready') $win.focus() }) // 加载地址 $win.loadURL(`file://${__dirname}/window/shortcut-capture.html`) $windows.push($win) } // 让窗口全屏 function setFullScreen ($win, display) { $win.setBounds({ width: display.size.width, height: display.size.height, x: display.bounds.x, y: display.bounds.y }) $win.setAlwaysOnTop(true) $win.setFullScreen(true) } // 关闭窗口 function closeWindow () { isClose = true while ($windows.length) { const $winItem = $windows.pop() $winItem.close() } isClose = false }复制代码
- 主进程与渲染进程通信通过ipcMain模块完成,ipcMain通过监听渲染进程传过来的事件获得渲染进程的数据,并且两个进程通信数据只能是简单对象。主进程向渲染进程传递数据是通过webContents的send方法实现的,渲染进程通过ipcRender对象事件监听实现,同是主进程也可以通过
webContents.executeJavaScript
方法以字符串的方式向页面注入js进行执行。 - 当程序运行之后,当用户按下快捷键后,主窗口的渲染进程就开始截图,截图后就把数据传到主进程,然后主进程创建新窗口,并把截图数据传递到新创建的窗口中,然后等待用户的截图操作
// 主进程捕获到截图快捷键就让渲染进程截图 ipcRenderer.on('shortcut-capture', () => { // 获取屏幕数量 // screen为electron的模块 const displays = screen.getAllDisplays() // 每个屏幕都截图一个 // desktopCapturer.getSources可以一次获取所有桌面的截图 // 但由于thumbnailSize不一样所以就采用了每个桌面尺寸都捕获一张 const getDesktopCapturer = displays.map((display, i) => { return new Promise((resolve, reject) => { desktopCapturer.getSources({ types: ['screen'], thumbnailSize: display.size }, (error, sources) => { if (!error) { return resolve({ display, thumbnail: sources[i].thumbnail.toDataURL() }) } return reject(error) }) }) }) Promise.all(getDesktopCapturer) .then(sources => { // 把数据传递到主进程 ipcRenderer.send('shortcut-capture', sources) }) .catch(error => console.log(error)) })复制代码
- 在本项目就采用了
webContents.executeJavaScript
的方法向页面传递了截图数据的 - 渲染进程接收到主进程的dom-ready事件之后就开始绘制截图界面,并把页面拖拽截取图片功能初始化。当用户按下
ESC
按键的时候就关闭截图窗口退出截屏 - 图片裁剪功能。图片裁剪是利用了canvas来实现的。canvas可以根据一张图片来绘制出图形,然后利用canvas的api把绘制出来的图片给获取成为可用的图片资源,然后提交给主进程。其中主要利用了canvas的
ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
方法- 其中image为图片资源
- sx、sy为原始图片资源要绘制的开始的位置
- sWidth、sHeight为原始图片资源要绘制大小
- dx、dy为把图片绘制到画布的起始位置
- dWidth、dHeight为把图片在画布上绘制的大小
- 本项目中sx、sy、 sWidth、sHeight都为截取的区域大小和区域相对于窗口左上角的坐标位置,dx、dy都为0,表示从画布的左上角开始绘制,dWidth、dHeight为截取区域大小,如果dWidth、dHeight和sWidth、sHeight不相等就可以实现截取区域的缩放,但本项目是1:1的
- 截取玩图片之后点击截图工具栏的确定按钮,然后就会从canvas读取图片信息,然后转换为dataURL传到主进程,主进程就把图片数据写入到剪切板并关闭窗口
- 由于截图窗口渲染进程的代码较多,这里就不上了,可以在Github上查看,下附整个截图的流程关系
示意图.png
最后,如果有时间的话,可也在考虑可以把截图这个功能单独提取出来然后做成一个模块,能够在其他electron项目中直接引用即可。写得不好的地方请各位大佬包容,GitHub项目地址:github.com/nashaofu/di…