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⭐,让更多人知道这个工具,感谢大家🙏

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/878248.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实习项目|苍穹外卖|day1

碎碎念 眨眼间&#xff0c;留给自己的时间不多了。想要去好的公司实习&#xff0c;现在是八股不会背&#xff0c;算法题全忘&#xff0c;跟了好多教程&#xff0c;也没有能写上简历的项目。因此&#xff0c;我决定用两个月的时间学习两个能够写到简历上的项目&#xff08;的确…

Android Studio:模拟器页面闪烁,手机模拟器输入画面闪烁 android studio闪屏

主要解决&#xff0c;android studio 启动app测试&#xff0c;输入数据时&#xff0c;手机画面就会闪烁&#xff0c;闪屏 1. 如图所示&#xff0c;依照顺序找到Edit &#xff0c;并点击Edit 2. 找到Graphics 选择为SoftWare &#xff0c;并保存修改即可 3. 如果此处不能选择S…

国内AI工具分类大盘点,这些神器你都用过了吗?

AI爆发到现成已经快2年了&#xff0c;基本上我自己也使用了近2年的AI产品。国内、外的AI产品体验了很多。 从最初文本聊天类的gpt、new bing、文心一言、通义千问&#xff0c;到后面绘图类Midjourney、Stable Diffusion、文心一格、通义万相等等。 在这里来分享我自己使用的一…

jmeter连接mysql数据库以及常规用法

1、在jmeter中新建一个测试计划&#xff0c;在测试计划界面中点击浏览&#xff0c;选择连接mysql数据库的jar包 如果没有jar包可以去网上下载&#xff0c;也可以通过如下链接进行下载 链接: https://pan.baidu.com/s/1BI6f19KSzXGlkSOwbnequw 提取码: gn8e 2、然后创建线程组&a…

SpringBoot日常:Spring之@PostConstruct解析

简介 spring的Bean在创建的时候会进行初始化&#xff0c;而初始化过程会解析出PostConstruct注解的方法&#xff0c;并反射调用该方法。 PostConstruct 的使用和特点 只有一个非静态方法能使用此注解&#xff1b;被注解的方法不得有任何参数&#xff1b;被注解的方法返回值必…

npm登录

npm 登录问题 npm login --auth-typelegacy报错 原因 npm源非npm本身源&#xff0c;需要切换&#xff1a; 查看源 nrm ls切换为npm源 nrm use npm重新登录 输入OTP验证后登录成功&#xff1a;

python从入门到精通:数据可视化-图形开发

1、json数据格式 • Json是一种轻量级的数据交互格式&#xff0c;可以按照Json指定的格式去组织和封装数据 • Json本质上是一种带有特殊格式的字符串 主要功能&#xff1a;json就是一种在各个编程语言中流通的数据格式&#xff0c;负责不同编程语言中的数据传递和交互&#xf…

【DSP+FPGA】基于2 个TMS320C6678+ XC7VX690T FPGA 的6U VPX 总线架构的高性能实时信号处理平台

6U VPX架构&#xff0c;符合VITA46规范板载 2 个TMS320C6678 多核DSP处理节点板载 1 片 XC7VX690T FPGA处理节点板载 2 个FMC 接口背板之间具有 4 路 x4 高速 GTH 互联&#xff0c;支持RapidIO、PCI ExpressFPGA 与 DSP 之间采用高速Rapid IO互联 基于6U VPX架构的高性能实时信…

BERT:Pre-training of Deep Bidirectional Transformers forLanguage Understanding

个人觉着BERT是一篇读起来很爽的论文 摘要 我们引入了一种新的语言表示模型BERT&#xff0c;它代表Bidirectional Encoder Representations from Transformers。与最近的语言表示模型不同(Peters et al.&#xff0c; 2018a;Radford et al.&#xff0c; 2018)&#xff0c; BER…

组合式API-reactive和ref函数,computed计算属性,watch函数

一.reactive&#xff08;&#xff09;接收一个对象类型的数据&#xff0c;返回一个响应式的对象&#xff1a; <script setup> import {reactive} from vue const state reactive({count:100 }) const setCount () > {state.count } </script> <template>…

书生大模型实战营第三期进阶岛第三课——LMDeploy 量化部署实践

LMDeploy 量化部署实践 任务一&#xff1a;创建虚拟环境创建文件夹LMDEPLOY用于存放课程相关的文件创建模型软连接LMDeploy验证启动模型文件LMDeploy API部署InternLM2.5-1.8b以命令行形式连接API服务器以Gradio网页形式连接API服务器LMDeploy KV量化W4A16 模型量化和部署W4A16…

create-react-app 移除 ESLint 语法检查

ESLint 的作用&#xff1a; ESLint 是一个流行的 JavaScript 代码静态检查工具&#xff0c;旨在帮助开发者识别和修复代码中的问题。以下是关于 ESLint 的一些关键信息&#xff1a; 主要功能&#xff1a; 1.代码风格检查&#xff1a;ESLint 可以检查代码是否符合特定的编码风…

经典算法之链表篇(二)

目录 一&#xff1a;重排链表&#xff08;LeetCode.143&#xff09; 二&#xff1a;删除链表的节点&#xff08;LCR 136. 删除链表的节点&#xff09; 三&#xff1a;K个一组反转链表&#xff08;LeetCode.25&#xff09; 有关经典算法链表的第一篇内容&#xff0c;可以查看我…

ESXi服务器无法安装Windows11:“不符合此版本的Windows所需最低系统要求“

目录 一、问题描述1.使用环境2.问题截图3.问题解析 二、解决方法Ⅰ1.按 ShiftF10 弹出命令提示符2.在弹出的Dos框中输入regedit&#xff0c;回车&#xff0c;进入注册表。3.打开HKEY_LOCAL_MACHINE\SYSTEM\Setup&#xff0c;并新建 LabConfig 的项&#xff0c;在 LabConfig 下创…

使用预训练的 ONNX 格式的 YOLOv8n 模型进行目标检测,并在图像上绘制检测结果

目录 __init__方法&#xff1a; pre_process方法&#xff1a; run方法&#xff1a; filter_boxes方法&#xff1a; view_img方法&#xff1a; __init__方法&#xff1a; 初始化类的实例时&#xff0c;创建一个onnxruntime的推理会话&#xff0c;加载名为yolov8n.onnx的模型…

C#开发基础之100个常用的C#正则表达式

前言 正则表达式是处理字符串的强大工具&#xff0c;特别是在文本搜索、替换和验证中。本文将100个常用的C#正则表达式进行分类&#xff0c;以帮助我们更快速地找到适合的正则表达式解决方案。 1. 基础匹配 这些正则表达式用于匹配一些基本的字符或字符串模式。 匹配任意字…

利用 Pytest Cache Fixture 实现中间数据缓存

接口自动化过程中&#xff0c;经常会遇到这样一些场景&#xff0c;“请求2需要用到请求1响应的数据”&#xff0c;常见的做法&#xff0c;进行用例依赖或者将请求1的响应结果写入一个文件&#xff0c;用到的时候读取文件。 当然这都不是这篇文章的重点&#xff0c;本片文章主要…

macos MacPort 包管理工具安装和使用

在macos v10.15版本中, xz, python等软件无法使用brew安装, 原因是brew对于旧版本的macos不再支持, 但是我们可以使用另外一个macos下的包管理工具来安装brew无法安装的软件, macport 是一个和brew类似的macos下的一个非常优秀的软件包安装管理工具. MacPort安装前提条件 安…

假期学习--对象底层结构和继承链

OC本质底层实现转化其实都是C/C代码。 OC对象的本质就是结构体。 NSObject底层是struct objc_object结构体 &#xff1b;struct objc_class : objc_object { …省略无关代码 // Class ISA; //ISA(从objc_object继承过来的) Class superclass; //指向其父类 cache_t cache…

旅游行业怎么利用C#接口发送短信

旅游企业一般拥有众多的分支机构&#xff0c;同时各地分支机构又有众多下属分散在当地各区的旅游营业报名点&#xff0c;以前传统的解决方案是采用专线、MODEM拔号等方式&#xff0c;专线的成本很高&#xff0c;MODEM拔号更费时&#xff0c;且长途拔号互联成本在多点情况下费用…