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

news2024/9/29 22:16:27

实现效果

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

    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.coloradmin.cn/o/2087712.html

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

相关文章

WEB应用服务器TOMCAT知识点

TOMCAT介绍 Tomcat是一个开源的Java Web应用服务器&#xff0c;主要用于运行Java编写的网站。 Apache Tomcat是由Apache Software Foundation&#xff08;ASF&#xff09;开发的一个开源Java Web应用服务器&#xff0c;最初由Sun Microsystems捐赠给Apache软件基金会&#xf…

数据结构(Java实现):栈和队列相关练习题

文章目录 1. 题目链接2. 题目解析2.1 括号匹配2.2 逆波兰表达式求值2.3 出栈入栈次序匹配2.4 最小栈2.5 环形数组队列2.6 用队列实现栈2.7 用栈实现队列 1. 题目链接 括号匹配逆波兰表达式求值出栈入栈次序匹配最小栈设计循环队列用队列实现栈用栈实现队列 2. 题目解析 2.1 …

基于RK3568平台移植ffmpeg3.4.5及ffmpeg验证

目录 一、概述二、环境要求2.1 硬件环境2.2 软件环境三、移植流程3.1 编译x2643.2 编译mpp3.3 编译ffmpeg四、ffmpeg验证4.1 ffmpeg配置说明4.2 ffmpeg推流/拉流使用说明4.2.1 使用http方式推流/拉流4.2.1.1 先执行ffmpeg服务4.2.1.2 再执行ffmpeg进行推流4.2.1.3 最后执行vlc进…

等保测评中的数据安全保护:重点与挑战

在信息安全等级保护&#xff08;等保&#xff09;测评中&#xff0c;数据安全保护是核心关注点之一&#xff0c;它不仅关系到企业的合规性&#xff0c;还直接影响到企业的运营安全和用户信任。本文将深入探讨等保测评中数据安全保护的重点与挑战&#xff0c;为企业提供有效的应…

JavaScript初级——事件传播

1、事件的传播 关于事件的传播网景公司和微软公司有不同的理解&#xff1a; 微软公司认为事件应该是由内向外传播&#xff0c;也就是当事件触发时&#xff0c;应该先触发当前元素上的事件&#xff0c;然后再向当前元素的祖先元素上传播&#xff0c;也就说事件应该在冒泡阶段执行…

如何解决U盘无法压缩卷或删除卷的问题

U盘在日常使用中&#xff0c;偶尔会遇到无法压缩卷或删除卷的情况。出现这些问题通常与U盘的磁盘状态或文件系统有关。本文将介绍一种有效的解决方法&#xff0c;通过使用Windows自带的磁盘管理工具diskpart来解决这些问题。 一、问题原因 U盘无法压缩卷或删除卷的常见原因包…

Nginx部署Vue前端项目全攻略:从构建到上线一步到位!

要将前端 Vue 项目部署到 Nginx&#xff0c;你需要遵循以下步骤&#xff1a; 首先确保你已经安装了 Node.js 和 npm。如果没有&#xff0c;请访问 Node.js 官网 下载并安装。 使用 Vue CLI 创建一个新的 Vue 项目&#xff08;如果你还没有一个&#xff09;&#xff1a; npm i…

探索未知,悦享惊喜 —— 您的专属盲盒小程序,即将开启奇妙之旅

在这个充满无限可能的数字时代&#xff0c;每一次点击都可能是通往惊喜的门户。我们匠心打造的“惊喜盲盒”小程序&#xff0c;正是为了给您带来前所未有的娱乐体验与心灵触动。在这里&#xff0c;每一份盲盒都蕴藏着精心挑选的宝藏&#xff0c;等待着与您的不期而遇。 【探索…

学习bat脚本

内容包含一些简单命令或小游戏&#xff0c;在乐趣中学习知识。 使用方法&#xff1a; 新建文本文档&#xff0c;将任选其一代码保存到文档中并保存为ASCII编码。将文件后缀改为.bat或.cmd双击运行即可。 一. 关机脚本 1. 直接关机 echo off shutdown -s -t 00秒直接关机。 2…

H5手机端调起支付宝app支付

1.调起APP页面如下 步骤 1.让后端对接一下以下文档&#xff08;手机网站支付通过alipays协议唤起支付宝APP&#xff09; https://opendocs.alipay.com/open/203/107091?pathHash45006f4f&refapi 2.后端接口会返回一个form提交表单 html&#xff1a;在页面中定义一个d…

halcon2

halcon自带图片路径 C:\Users\Public\Documents\MVTec\HALCON-18.11-Progress\examples 案例1&#xff1a;blob 固定阈值分割图像-车牌号识别 案例2&#xff1a;blob 动态阈值分割 匹配字母 案例1:打开窗口并画几何图形 &#xff08;ROI 感兴趣区域&#xff09; 并且距离测量…

IO进程day05(线程、同步、互斥、条件变量、进程间通信IPC)

目录 【1】线程 1》什么是线程 1> 概念 2> 进程和线程的区别 3> 线程资源 2》 函数接口 1> 创建线程&#xff1a;pthread_create 2> 退出线程&#xff1a;pthread_exit 3> 回收线程资源 练习1&#xff1a;通过父子进程完成对文件的拷贝&#xff08…

sqlmap注入语句学习,帮助你排查SQL注入漏洞

摘要 sqlmap是一个开源的渗透测试工具&#xff0c;可以用来进行自动化检测&#xff0c;利用SQL注入漏洞&#xff0c;获取数据库服务器的权限。它具有功能强大的检测引擎&#xff0c;针对各种不同类型数据库的渗透测试的功能选项&#xff0c;包括获取数据库中存储的数据&#x…

第3章-03-Python库Requests安装与讲解

Python库Requests的安装与讲解 &#x1f3c6;作者简介&#xff0c;黑夜开发者&#xff0c;CSDN领军人物&#xff0c;全栈领域优质创作者✌&#xff0c;CSDN博客专家&#xff0c;阿里云社区专家博主&#xff0c;2023年CSDN全站百大博主。 &#x1f3c6;数年电商行业从业经验&…

GPLGIAGQ;MMP2靶向光敏剂多肽;MMP2可降解 (cleavable) 的多肽;CAS号:109053-09-0

【GPLGIAGQ 简介】 GPLGIAGQ 是一种 MMP2 可切割的多肽,在脂质体和胶束纳米载体中都被用作刺激敏感的连接物,用于 MMP2 触发的肿瘤靶向治疗。GPLGIAGQ可用于合成光动力治疗 (PDT) 中独特的MMP2靶向光敏剂。 【中文名称】MMP2靶向光敏剂多肽&#xff1b;GPLGIAGQ 【英文名称】G…

【知识】缓存类型和策略

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 目录 缓存读取策略 缓存写入策略 直写缓存写入策略 回写高速缓存写入策略 回写缓存写入策略 ​​​​​​缓存替换策略 先进先出 &#xff08;F…

C与C++的三种区分方式

1、单个字符的sizeof大小 在C和C中&#xff0c;单个字符&#xff08;char类型&#xff09;的大小通常是1字节&#xff08;8位&#xff09;&#xff0c;但这取决于编译器和目标平台。这是一种特别的区分方式&#xff0c;特别定义的。 2、是否有__cplusplus __cplusplus是一个预…

【html+css 绚丽Loading】000025 玄机翻转棱

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享htmlcss 绚丽Loading&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495…

等保2.0 | Apache Tomcat中间件测评

这里就谈谈等保2.0要求&#xff0c;对应到Apache Tomcat中间件的一些条款要求。 安装步骤略过&#xff0c;我们直接看等保中涉及的一些参数。 首先&#xff0c;做测评的时候我们先要记录相应的软件版本&#xff1a; 查看版本&#xff0c;在tomcat目录下执行/bin/catalina.sh…

是否应该使用WordPress自动更新的功能

开源程序例如WordPress&#xff0c;使许多人能够轻松创建自己的网站。然而&#xff0c;却存在一个棘手的问题是黑客攻击。开源的性质及其安全透明性让黑客、机器人和脚本小子提供了不断攻击的机会。防止WordPress网站被黑的首要方法是保持WordPress版本、主题和插件的更新。对于…