Создайте инструмент скриншота с помощью электрона с нуля

внешний интерфейс JavaScript Electron Canvas

Недавно я пытаюсь использовать электрон, чтобы обернуть веб-версию инструмента чата в настольное приложение. Можно сказать, что в качестве инструмента чата скриншоты являются обязательной функцией. Но, к сожалению, я не нашел очень зрелой библиотеки для использования, или она может быть открыта не так, короче, я не видел готовой библиотеки, поэтому я хотел создать простой инструмент для создания скриншотов с нуля. . Давайте перейдем к следующему пункту!


идеи

Electron предоставляет API для скриншотов, который может легко получить информацию об изображении каждого экрана (в случае внешнего монитора) и каждого окна.

  1. Сделайте снимок экрана, затем создайте полноэкранное окно, чтобы покрыть весь экран, нарисуйте захваченное изображение на окне, а затем покройте его слоем черных полупрозрачных элементов, что выглядит так, как будто экран зафиксирован;
  2. Добавить эффект интерактивного выделения в окне;
  3. Нажмите OK, используйте положение холста, соответствующее выделению, для захвата содержимого изображения, записи в буфер обмена и сохранения изображения.

Построить проект

Сначала создайтеpackage.jsonЗаполните необходимую информацию для проекта, обратите внимание, что основным является входной файл.

{
  "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, код взят из электронной официальной документации

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>

Такой простой электронный проект завершен, выполнитеyarn startилиnpm startВы можете увидеть окно с кнопкой в ​​окне

вызвать скриншот

Скриншот — относительно независимая функция, и могут быть глобальные сочетания клавиш и триггеры меню, которые покидают окно, поэтому триггер скриншотов должен быть реализован в основном процессе.

Это можно сделать через ipc-связь в процессе рендеринга, используя ipcRenderer для отправки событий в коде страницы и используя ipcMain в main для получения событий.

// index.html
	const { ipcRenderer } = require('electron')

	document.getElementById('js-capture').addEventListener('click', ()=>{
		ipcRenderer.send('capture-screen')
	})

Получено в основном процессе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
    })

}

Окно должно покрывать весь экран и быть полностью сверху, что можно использовать под окнами.fullscreenЧтобы обеспечить полноэкранный режим, полноэкранный режим под Mac переместит окно на отдельный рабочий стол, поэтому используется другой метод: в комментариях к коду отмечены соответствующие параметры разных систем, а конкретное содержимое можно просмотреть в документе.

Обратите внимание, что это окно загружает еще один 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 : Скриншот изображения Маска: серая маска Холст: рисует выбранную область изображения и границу Информация о размере: определяет размер диапазона перехвата. Панель инструментов: кнопка действия, используемая для отмены и сохранения и т. д. 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;
}

Каждый элемент в основном позиционируется абсолютно, а позиция контролируется js Кнопка использует iconfont , все задействованные файлы ресурсов и полные проекты можно найти по адресуGitHub - chrisbing/electorn-capture-screen: electron capture screenскачать

взаимодействие со скриншотом

Завершенные функции включают фотографирование указанной области, перетаскивание для перемещения и изменение размера выделения, отображение размера в реальном времени и панель инструментов.

сделать скриншот

// 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Смотреть на

При инициализации сохранить холст, рисующий все картинки, который используется для последующего выбора некоторых картинок в области выделения.

В процессе рисования от холста черезgetImageDataПолучите содержимое изображения, а затем передайтеputImageDataрисовать на холсте дисплея

Дополнительный контент

Выбор изображения обрабатывается в классе 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Создайте изображение для записи в буфер обмена, уведомите основной процесс о том, что снимок экрана завершен, и прикрепите URL-адрес изображения в формате base64, а затем закройте окно.

сохранить в файл

// 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. , и программа закрылась. 😂