从零开始用 electron 手撸一个截屏工具

栏目: Html5 · 发布时间: 5年前

内容简介:最近在尝试利用 electron 将一个 web 版的聊天工具包装成一个桌面 APP。作为一个聊天工具,截屏可以说是一个必备功能了。不过遗憾的是没有找到很成熟的库来用,也可能是打开方式不对,总之呢没看到现成的,于是就想从头撸一个简单的截图工具。下面就进入正题吧!electron 提供了截取屏幕的 API,可以轻松的获取每个屏幕(存在外接显示器的情况)和每个窗口的图像信息。首先创建

最近在尝试利用 electron 将一个 web 版的聊天 工具 包装成一个桌面 APP。作为一个聊天工具,截屏可以说是一个必备功能了。不过遗憾的是没有找到很成熟的库来用,也可能是打开方式不对,总之呢没看到现成的,于是就想从头撸一个简单的截图工具。下面就进入正题吧!

思路

electron 提供了截取屏幕的 API,可以轻松的获取每个屏幕(存在外接显示器的情况)和每个窗口的图像信息。

  1. 把图片截取出来,然后创建一个全屏的窗口盖住整个屏幕,将截取的图片绘制在窗口上,然后再覆盖一层黑色半透明的元素,看起来就像屏幕定住了一样;
  2. 在窗口上增加交互制作选区的效果;
  3. 点击确定,利用 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 触摸板五指张开的手势隐藏了窗口才关掉了程序,:joy:


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Agile Web Development with Rails 4

Agile Web Development with Rails 4

Sam Ruby、Dave Thomas、David Heinemeier Hansson / Pragmatic Bookshelf / 2013-10-11 / USD 43.95

Ruby on Rails helps you produce high-quality, beautiful-looking web applications quickly. You concentrate on creating the application, and Rails takes care of the details. Tens of thousands of deve......一起来看看 《Agile Web Development with Rails 4》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具