实现效果
实现思路
- 创建两个window,一个叫mainWindow,一个叫cutWindow
- mainWindow:主界面用来展示截图结果
- cutWindow:截图窗口,加载截图页面和截图交互逻辑
- mainWindow 页面点击截图,让cutWIndow 来实现具体截图逻辑
- cutWindow:截图完后把截图send给mainWindow页面
截图过程-时序图
创建项目
我在网上找了一大圈,没有找到一个合适的模板,要么环境太老、要么配置各种缺失不完善、要么打包出来各种问题等等,说实话坑还真不少,无意间找到一个特别好的脚手架,它简单又完善。推荐给大家:electron-vite ,所以接下来直接用创建命令
jsx
yarn create @quick-start/electron
安装依赖
- vue-router:切换加载首页和截图页面
- konva:完成截图交互的库
jsx
yarn add konva vue-router
核心代码
为了更好的展示添加的内容,提供如下目录结构图方便理解
目录结构
主进程
-
src/main/index.js
jsximport { app, shell, BrowserWindow, ipcMain, screen, desktopCapturer, globalShortcut } from 'electron' import { join } from 'path' import { electronApp, optimizer, is } from '@electron-toolkit/utils' import icon from '../../resources/icon.png?asset' let mainWindow let cutWindow function closeCutWindow() { cutWindow && cutWindow.close() cutWindow = null } function createMainWindow() { mainWindow = new BrowserWindow({ width: 900, height: 670, show: false, autoHideMenuBar: true, ...(process.platform === 'linux' ? { icon } : {}), webPreferences: { preload: join(__dirname, '../preload/index.js'), sandbox: false } }) mainWindow.on('ready-to-show', () => { mainWindow.show() }) mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: 'deny' } }) console.log('loadURL:', process.env['ELECTRON_RENDERER_URL']) if (is.dev && process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } mainWindow.on('closed', () => { closeCutWindow() }) } function registerShortcut() { //! 截图快捷键 globalShortcut.register('CommandOrControl+Alt+C', () => { openCutScreen() }) globalShortcut.register('Esc', () => { closeCutWindow() mainWindow.show() }) globalShortcut.register('Enter', sendFinishCut) } app.whenReady().then(() => { // Set app user model id for windows electronApp.setAppUserModelId('com.electron') // Default open or close DevTools by F12 in development // and ignore CommandOrControl + R in production. // see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils //! 开发模式:win 环境F12 和 mac os 环境:CommandOrControl + R 打开 DevTools app.on('browser-window-created', (_, window) => { optimizer.watchWindowShortcuts(window) }) createMainWindow() registerShortcut() openMainListener() app.on('activate', function () { if (BrowserWindow.getAllWindows().length === 0) createMainWindow() }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') { globalShortcut.unregisterAll() app.quit() } }) function getSize() { const { size, scaleFactor } = screen.getPrimaryDisplay() return { width: size.width * scaleFactor, height: size.height * scaleFactor } } function createCutWindow() { const { width, height } = getSize() cutWindow = new BrowserWindow({ width, height, autoHideMenuBar: true, useContentSize: true, movable: false, frame: false, resizable: false, hasShadow: false, transparent: true, fullscreenable: true, fullscreen: true, simpleFullscreen: true, alwaysOnTop: false, webPreferences: { preload: join(__dirname, '../preload/index.js'), nodeIntegration: true, contextIsolation: false } }) console.log('createCutWindow:', is.dev, process.env['ELECTRON_RENDERER_URL']) if (is.dev && process.env['ELECTRON_RENDERER_URL']) { let url = process.env['ELECTRON_RENDERER_URL'] + '/#/cut' console.log('createCutWindow: loadURL=', url) cutWindow.loadURL(url) } else { cutWindow.loadFile(path.join(__dirname, '../renderer/index.html')) } cutWindow.maximize() cutWindow.setFullScreen(true) } function sendFinishCut() { cutWindow && cutWindow.webContents.send('FINISH_CUT') } function openCutScreen() { closeCutWindow() mainWindow.hide() createCutWindow() cutWindow.show() } function openMainListener() { ipcMain.on('OPEN_CUT_SCREEN', openCutScreen) ipcMain.on('SHOW_CUT_SCREEN', async (e) => { let sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: getSize() }) cutWindow && cutWindow.webContents.send('GET_SCREEN_IMAGE', sources[0]) }) ipcMain.on('FINISH_CUT_SCREEN', async (e, cutInfo) => { closeCutWindow() mainWindow.webContents.send('GET_CUT_INFO', cutInfo) mainWindow.show() }) ipcMain.on('CLOSE_CUT_SCREEN', async (e) => { closeCutWindow() mainWindow.show() }) }
渲染器
-
scr/main.js
jsximport { createApp } from 'vue' import App from './App.vue' import router from './router' const app = createApp(App) app.use(router) app.mount('#app')
-
src/router/index.js
jsximport { createRouter, createWebHashHistory } from 'vue-router' const routes = [ { path: '/', redirect: '/home' }, { path: '/home', name: 'home', component: () => import('../pages/Home/index.vue') }, { path: '/cut', name: 'cut', component: () => import('../pages/Cut/index.vue') } ] const router = createRouter({ history: createWebHashHistory(), routes }) export default router
-
src/App.vue
jsx<template> <router-view> </router-view> </template> <script setup> </script> <style lang="less"> @import './assets/css/styles.less'; </style>
-
src/pages/index.vue:首页
jsx<template> <div class="container"> <button @click="handleCutScreen">截屏</button> <div> <img :src="previewImage" style="max-width: 100%" /> </div> </div> </template> <script setup> import { ref } from "vue"; const { ipcRenderer } = window.electron; const previewImage = ref(""); async function handleCutScreen() { await ipcRenderer.send("OPEN_CUT_SCREEN"); ipcRenderer.removeListener("GET_CUT_INFO", getCutInfo); ipcRenderer.on("GET_CUT_INFO", getCutInfo); } function getCutInfo(event, pic) { previewImage.value = pic; } </script>
-
src/pages/cut.vue:截图界面
jsx<template> <div class="container" :style="'background-image:url(' + bg + ')'" ref="container" @mousedown="onMouseDown" @mousemove="onMouseMove" @mouseup="onMouseUp"> </div> </template> <script setup> import Konva from "konva"; import { ref, onMounted } from "vue"; const { ipcRenderer } = window.electron; let container = ref(null); let bg = ref(""); let stage, layer, rect, transformer; onMounted(() => { ipcRenderer.send("SHOW_CUT_SCREEN"); ipcRenderer.removeListener("GET_SCREEN_IMAGE", getSource); ipcRenderer.on("GET_SCREEN_IMAGE", getSource); ipcRenderer.on("FINISH_CUT", confirmCut); }); async function getSource(event, source) { const { thumbnail } = source; const pngData = await thumbnail.toDataURL("image/png"); bg.value = pngData; render(); } function render() { stage = createStage(); layer = createLayer(stage); } function createStage() { return new Konva.Stage({ container: container.value, width: window.innerWidth, height: window.innerHeight, }); } function createLayer(stage) { let layer = new Konva.Layer(); stage.add(layer); layer.draw(); return layer; } function createRect(layer, x, y, width, height, alpha, draggable) { let rect = new Konva.Rect({ x, y, width, height, fill: `rgba(0,0,255,${alpha})`, draggable }); layer.add(rect); return rect; } let isDown = false; let rectOption = {}; function onMouseDown(e) { if (rect || isDown) { return; } isDown = true; const { pageX, pageY } = e; rectOption.x = pageX || 0; rectOption.y = pageY || 0; rect = createRect(layer, pageX, pageY, 0, 0, 0.25, false); rect.draw(); } function onMouseMove(e) { if (!isDown) return; const { pageX, pageY } = e; let w = pageX - rectOption.x; let h = pageY - rectOption.y; rect.remove(); rect = createRect(layer, rectOption.x, rectOption.y, w, h, 0.25, false); rect.draw(); } function onMouseUp(e) { if (!isDown) { return; } isDown = false; const { pageX, pageY } = e; let w = pageX - rectOption.x; let h = pageY - rectOption.y; rect.remove(); rect = createRect(layer, rectOption.x, rectOption.y, w, h, 0, true); rect.draw(); transformer = createTransformer(rect); layer.add(transformer); } function createTransformer(rect) { let transformer = new Konva.Transformer({ nodes: [rect], rotateAnchorOffset: 60, enabledAnchors: ['top-left', 'top-right', 'bottom-left', 'bottom-right'] }); return transformer } /** * 根据选择区域生成图片 * @param {*} info */ async function getCutImage(info) { const { x, y, width, height } = info; let img = new Image(); img.src = bg.value; let canvas = document.createElement("canvas"); let ctx = canvas.getContext("2d"); canvas.width = ctx.width = width; canvas.height = ctx.height = height; ctx.drawImage(img, -x, -y, window.innerWidth, window.innerHeight); return canvas.toDataURL("image/png"); } /** * 确认截图 */ async function confirmCut() { const { width, height, x, y, scaleX = 1, scaleY = 1 } = rect.attrs; let _x = width > 0 ? x : x + width * scaleX; let _y = height > 0 ? y : y + height * scaleY; let pic = await getCutImage({ x: _x, y: _y, width: Math.abs(width) * scaleX, height: Math.abs(height) * scaleY, }); ipcRenderer.send("FINISH_CUT_SCREEN", pic); } /** * 关闭截图 */ function closeCut() { ipcRenderer.send("CLOSE_CUT_SCREEN"); } </script> <style lang="scss" scoped> .container { position: fixed; top: 0; bottom: 0; left: 0; right: 0; width: 100%; height: 100%; overflow: hidden; background-color: transparent; background-size: 100% 100%; background-repeat: no-repeat; border: 2px solid blue; box-sizing: border-box; } </style>
总结
虽然实现了核心功能,但是仅支持主屏幕截图,不支持多屏幕截图,同时还遗留诸多问题,后面单独一篇更新解决
完整demo :传送门,顺便帮忙点个star,感谢~