当前位置: 首页 > news >正文

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Spark框架
  • 【kubernetes】持久化存储 —— PV / PVC
  • 打开mdk的configuration wizard界面
  • Qt:玩转QPainter序列九(文本,文本框,填充)
  • SpringBoot Web请求响应
  • 极盾故事|某金融租赁机构应用数据保护新策略:“动态脱敏”“二次授权”
  • Trm理论 2(Word2Vec)
  • 使用AI写WebSocket知识是一种怎么样的体验?
  • 【C++ Qt day5】
  • Docker 安装FileBeat、Elasticsearch及Kibana详细步骤
  • git查看代码提交记录
  • python使用selenium,实现简单爬虫功能
  • 9月4日星期三今日早报简报微语报早读
  • 太阳能光伏异常红外图像数据集
  • CentOS 7 docker 部署遇到内网通,外网不通 问题
  • 《Javascript高级程序设计 (第三版)》第五章 引用类型
  • 【Leetcode】104. 二叉树的最大深度
  • 【个人向】《HTTP图解》阅后小结
  • 【剑指offer】让抽象问题具体化
  • 2018一半小结一波
  • ES6语法详解(一)
  • exif信息对照
  • js中forEach回调同异步问题
  • leetcode386. Lexicographical Numbers
  • Mocha测试初探
  • PHP 小技巧
  • Redux 中间件分析
  • Vue.js-Day01
  • 它承受着该等级不该有的简单, leetcode 564 寻找最近的回文数
  • 我的业余项目总结
  • 学习使用ExpressJS 4.0中的新Router
  • scrapy中间件源码分析及常用中间件大全
  • # Spring Cloud Alibaba Nacos_配置中心与服务发现(四)
  • #ifdef 的技巧用法
  • (1)(1.13) SiK无线电高级配置(五)
  • (DFS + 剪枝)【洛谷P1731】 [NOI1999] 生日蛋糕
  • (第8天)保姆级 PL/SQL Developer 安装与配置
  • (附源码)python房屋租赁管理系统 毕业设计 745613
  • (附源码)计算机毕业设计SSM在线影视购票系统
  • (每日持续更新)信息系统项目管理(第四版)(高级项目管理)考试重点整理第3章 信息系统治理(一)
  • (四)模仿学习-完成后台管理页面查询
  • (未解决)jmeter报错之“请在微信客户端打开链接”
  • (学习日记)2024.04.04:UCOSIII第三十二节:计数信号量实验
  • (一)kafka实战——kafka源码编译启动
  • (一)硬件制作--从零开始自制linux掌上电脑(F1C200S) <嵌入式项目>
  • (已解决)Bootstrap精美弹出框模态框modal,实现js向modal传递数据
  • (转)Spring4.2.5+Hibernate4.3.11+Struts1.3.8集成方案一
  • **CI中自动类加载的用法总结
  • .net core MVC 通过 Filters 过滤器拦截请求及响应内容
  • .netcore 如何获取系统中所有session_如何把百度推广中获取的线索(基木鱼,电话,百度商桥等)同步到企业微信或者企业CRM等企业营销系统中...
  • .sh
  • @antv/x6 利用interacting方法来设置禁止结点移动的方法实现。
  • @ModelAttribute 注解
  • @require_PUTNameError: name ‘require_PUT‘ is not defined 解决方法
  • [ 网络基础篇 ] MAP 迈普交换机常用命令详解