用electron开发了一个屏幕截图工具

前段时间做了一个钉钉的Linux版本,由于是基于网页版做的,所以缺失了很多桌面应用程序的功能。由于使用的用户多是Linux的用户,所以在Linux的截图功能没有,在几个用户的要求下决定做一个截图功能。

项目目前支持显示器截图,在windows上运行效果比较理想,Linux上有一定的BUG,目前还不能够支持跨屏幕截图(一个截图横跨两个显示器)功能。本文同时也发布在掘金https://juejin.im/post/59fddda56fb9a0452a3bb182

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 <index>; 如果资源为窗口, 名字为窗口的标题
      • thumbnail: 屏幕缩略图

屏幕截图功能编写

  1. 为了能够符合大多数用户的习惯,特别是用惯了QQ截图功能的小伙伴,所以使用了快捷键ctrl+alt+a来截图
  2. 所有的程序处理代码都必须等到app ready事件之后再处理,否则会报错,所以所有代码都放到了ready事件的回调函数中。
  3. 为了把截图功能给独立出来不与其他模块相互干扰,所以就把截图相关的主进程代码单独写到文件shortcut-capture.js,并把模块封装为一个函数,并且通过变量控制,保证整个应用进程内只会执行一次初始化截图模块。
  4. 主进程代码如下:
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
}
  1. 主进程与渲染进程通信通过ipcMain模块完成,ipcMain通过监听渲染进程传过来的事件获得渲染进程的数据,并且两个进程通信数据只能是简单对象。主进程向渲染进程传递数据是通过webContents的send方法实现的,渲染进程通过ipcRender对象事件监听实现,同是主进程也可以通过webContents.executeJavaScript方法以字符串的方式向页面注入js进行执行。
  2. 当程序运行之后,当用户按下快捷键后,主窗口的渲染进程就开始截图,截图后就把数据传到主进程,然后主进程创建新窗口,并把截图数据传递到新创建的窗口中,然后等待用户的截图操作
// 主进程捕获到截图快捷键就让渲染进程截图
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))
})
  1. 在本项目就采用了webContents.executeJavaScript的方法向页面传递了截图数据的
  2. 渲染进程接收到主进程的dom-ready事件之后就开始绘制截图界面,并把页面拖拽截取图片功能初始化。当用户按下ESC按键的时候就关闭截图窗口退出截屏
  3. 图片裁剪功能。图片裁剪是利用了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的
  1. 截取玩图片之后点击截图工具栏的确定按钮,然后就会从canvas读取图片信息,然后转换为dataURL传到主进程,主进程就把图片数据写入到剪切板并关闭窗口
  2. 由于截图窗口渲染进程的代码较多,这里就不上了,可以在Github上查看,下附整个截图的流程关系
    示意图.png

最后,如果有时间的话,可也在考虑可以把截图这个功能单独提取出来然后做成一个模块,能够在其他electron项目中直接引用即可。写得不好的地方请各位大佬包容,GitHub项目地址:https://github.com/nashaofu/dingtalk

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,980评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,422评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,130评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,553评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,408评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,326评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,720评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,373评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,678评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,722评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,486评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,335评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,738评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,283评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,692评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,893评论 2 335

推荐阅读更多精彩内容