Electron 项目实战 03: 实现一个截图功能

2024-08-29 11:20

本文主要是介绍Electron 项目实战 03: 实现一个截图功能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

实现效果

20240110195937.gif

实现思路

  1. 创建两个window,一个叫mainWindow,一个叫cutWindow
  2. mainWindow:主界面用来展示截图结果
  3. cutWindow:截图窗口,加载截图页面和截图交互逻辑
  4. mainWindow 页面点击截图,让cutWIndow 来实现具体截图逻辑
  5. cutWindow:截图完后把截图send给mainWindow页面

截图过程-时序图

%E6%88%AA%E5%9B%BE%E8%BF%87%E7%A8%8B.png

创建项目

我在网上找了一大圈,没有找到一个合适的模板,要么环境太老、要么配置各种缺失不完善、要么打包出来各种问题等等,说实话坑还真不少,无意间找到一个特别好的脚手架,它简单又完善。推荐给大家:electron-vite ,所以接下来直接用创建命令

yarn create @quick-start/electron

安装依赖

  • vue-router:切换加载首页和截图页面
  • konva:完成截图交互的库
yarn add konva vue-router

核心代码

为了更好的展示添加的内容,提供如下目录结构图方便理解

目录结构

Untitled.png

主进程

  • src/main/index.js

    import {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 cutWindowfunction 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 windowselectronApp.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 打开 DevToolsapp.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

    import { 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

    import { 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

    <template><router-view></router-view>
    </template><script setup>
    </script><style lang="less">
    @import './assets/css/styles.less';
    </style>
    
  • src/pages/index.vue:首页

    <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:截图界面

    <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,感谢~

参考文献

  • https://juejin.cn/post/7111115472182968327
  • https://www.electronjs.org/docs/latest/tutorial/keyboard-shortcuts
  • https://konvajs.org/docs/select_and_transform/Basic_demo.html
  • https://stackoverflow.com/questions/40360109/content-security-policy-img-src-self-data/62213224#62213224

更多

家人们,我最近花了2个多月开源了一个文章发布助手artipub,可以帮你一键将markdown发布至多平台(发布和更新),方便大家更好的传播知识和分享你的经验。
目前已支持平台:个人博客、Medium、Dev.to(未来会支持更多平台)
官网地址:https://artipub.github.io/artipub/
仓库地址:https://github.com/artipub/artipub

目前库已可以正常使用,欢迎大家体验、如果你有任何问题和建议都可以在Issue给我进行反馈。
如果你赶兴趣,特别欢迎你的加入,让咋们一起完善好这个工具。
帮忙点个star⭐,让更多人知道这个工具,感谢大家🙏

这篇关于Electron 项目实战 03: 实现一个截图功能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1117720

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

如何用Docker运行Django项目

本章教程,介绍如何用Docker创建一个Django,并运行能够访问。 一、拉取镜像 这里我们使用python3.11版本的docker镜像 docker pull python:3.11 二、运行容器 这里我们将容器内部的8080端口,映射到宿主机的80端口上。 docker run -itd --name python311 -p

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo