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

Electron 应用实现截图并编辑功能

Electron 应用实现截图并编辑功能

Electron 应用如何实现截屏功能,有两种思路,作为一个框架是否可以通过框架实现截屏,另一种就是 javaScript 结合 html 中画布功能实现截屏。
在初步思考之后,本文优先探索使用 Electron 实现截屏功能。作为一个成熟的框架,如果能够完成截屏,那自然是已经考虑了各种会出现的问题。
Electron 想要截屏还是要用到 desktopCapturer API。这个 API 也是用来实现录屏。
首先创建一个项目,直接 clone angular-electron。

环境

  • Angular@13.3.1
  • Electron@18.0.1
  • ngx-img-cropper@11.0.0

流程:

1.渲染进程向主进程取截屏的数据。
2.主进程获取截屏数据,并返回。
3.渲染进程取到数据后,将数据转为图片显示在页面上。
4.页面编辑图片并获取新的图片数据保存到本地。

首先在 home.component.ts 中绑定一个点击事件,向主进程发送一个消息取得录屏的初始数据:

async getScreensht() {let data = await this.electron.ipcRenderer.invoke("get-screenshot");
}

在主进程 main.ts 中,首先获取当前屏幕(可能存在多个屏幕),再取得当前屏幕的截屏数据:

先看取得截屏数据的方法:

let sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: thumbSize });

结果如下(只有一个屏幕数据,如果有两个屏幕,则有两条数据,依次类推):

 [{name: 'Entire Screen',id: 'screen:0:0',thumbnail: NativeImage {toPNG: [Function: toPNG],toJPEG: [Function: toJPEG],toBitmap: [Function: toBitmap],getBitmap: [Function: getBitmap],toDataURL: [Function: toDataURL],...},display_id: '2528732444',appIcon: null}
]

这个结果中有一个参数 display_id,代表着对应的屏幕。那么怎么知道截屏哪个屏幕呢?需要利用鼠标点击事件,鼠标在哪个屏幕点击则截屏哪个屏幕。
鼠标点击位于当前屏幕的窗口,方法如下,通过 BrowserWindow 找到聚焦的窗口,再根据位置判断当前窗口位于哪个屏幕:

// 获取当前窗口所在屏幕
function getCurrentScreen() {let focusedWindow = BrowserWindow.getFocusedWindow();let currentBounds =  focusedWindow.getBounds();let currentDisplay = screen.getAllDisplays().find((display) => {return (currentBounds.x >= display.bounds.x &&currentBounds.x < display.bounds.x + display.bounds.width &&currentBounds.y >= display.bounds.y &&currentBounds.y < display.bounds.y + display.bounds.height);});return currentDisplay;
}

以上方法返回的结果如下,可以看到其中的 id 参数与上文中的 display_id 一致。
由此可以从 desktopCapturer.getSources() 返回的多个数据中找到当前点击的屏幕。

{id: 2528732444,bounds: { x: 0, y: 0, width: 1920, height: 1080 },workArea: { x: 0, y: 0, width: 1920, height: 1040 },accelerometerSupport: 'unknown',...
}

遗憾的是在后续的测试中,竟然存在部分设备返回 currentDisplay 中的 id 参数为 “”(空字符串)。
这样,无法通过 display_id 与 id 的一一对应,而确定截取的是哪个屏幕。
为什么会出现这种情况?在 github 上 electron 的代码库中有此讨论。
请看这里 desktopCapturer display_id is empty string

根据讨论,另一种方法为下,

function getCurrentScreen() {let currentBounds = win.getBounds();let currentDisplay = screen.getDisplayNearestPoint({ x: currentBounds.x, y: currentBounds.y });let allDisplays = screen.getAllDisplays();let currentDisplayIndex = allDisplays.findIndex((display) => {return display.id === currentDisplay.id});return { 'screen_index': currentDisplayIndex };;
}

那么梳理一下流程:渲染进程响应一个点击事件,向主进程发送一个消息,获取当前屏幕的截屏数据:

// 渲染进程
let data = await this.electron.ipcRenderer.invoke("get-screenshot");// 主进程
ipcMain.handle('get-screenshot', async (e, args) => {let current_screen = getCurrentScreen();  // 取得当前屏幕let primaryDisplay = screen.getPrimaryDisplay();// 这里的 primaryDisplay.size 由于缩放的原因可能与系统设置的分辨率不一样, 再乘上缩放比 scaleFactorlet reality_width = primaryDisplay.size.width * primaryDisplay.scaleFactor;let reality_height = primaryDisplay.size.height * primaryDisplay.scaleFactor;let thumbSize = { width: reality_width, height: reality_height };let source = await getDesktopCapturer(current_screen, thumbSize); // 取得当前屏幕截屏数据if (source) {return source;}
});async function getDesktopCapturer(current_screen, thumbSize) {let screenName = current_screen['screen_index'] + 1;let screen_names = [];screen_names.push('Screen ' + screenName);  // 中文为 `screen_names.push('屏幕 ' + screenName);`screen_names.push('Entire Screen');  // 中文为 `screen_names.push('整个屏幕');`// 以 thumbSize 屏幕分辨率取得所有屏幕截屏数据,如果 types 设置为 ['screen', 'window'] 同时可以获取各个窗口的截屏数据let sources = await desktopCapturer.getSources({ types: ['screen'], thumbnailSize: thumbSize });// 如果只有一个屏幕,则 name 为'整个屏幕',如果有两个及以上屏幕,则 name 为 '屏幕 1' 和 '屏幕 2'if (sources) {for (let source of sources) {if (screen_names.indexOf(source.name) != -1) {  // 通过 name 确定屏幕return source;}}}
}

渲染进程中取到的截屏数据如下:

{name: 'Entire Screen',id: 'screen:0:0',thumbnail: NativeImage {toPNG: [Function: toPNG],toJPEG: [Function: toJPEG],toBitmap: [Function: toBitmap],getBitmap: [Function: getBitmap],toDataURL: [Function: toDataURL],...},display_id: '2528732444',appIcon: null
}

thumbnail 为一个对象,通过其中的 toPNG、toJPG、toDataURL 等方法可以将数据转为 PNG、JPG 等格式。
例如以下转为 dataURL,即 base64 编码格式,以便在 web 中显示在 img 标签中:

let data = await this.electron.ipcRenderer.invoke("get-screenshot");
let image_url = data.thumbnail.toDataURL();

又或者在主进程中先转为 PNG 格式 let png_data = data.thumbnail.toPNG();
再使用 fs 模块直接保存到本地 fs.writeFileSync('D:\\1.png', png_data);

在渲染进程中得到了截屏数据,然后就是显示和编辑。

这里选取 ngx-img-cropper 插件。安装 npm i ngx-img-cropper@11.0.0 --save,由于本项目使用 Angular@13.3.1 所以使用 v11.0.0 版本。
ngx-img-cropper 教程。

在 module.ts 中导入 import { ImageCropperModule } from 'ngx-img-cropper';

然后根据教程中 Customizing Image cropper 一节内容这里做如下修改:

home.conponent.html 文件内容如下,去掉多余的选择文件和预览显示,留下编辑部分,再加上三个 button,用于获取截屏,清除截屏,和保存结果。

<div class="container"><div style="display: flex;"><button (click)="getScreensht()">get</button><button (click)="clear()">clear</button><button (click)="save()">save</button></div><img-cropper #cropper [image]="image_data" [settings]="cropperSettings"></img-cropper>
</div>

home.component.ts 文件修改如下,首先修改 constructor 中的内容,

this.cropperSettings = new CropperSettings();
this.cropperSettings.preserveSize = true;  // 不缩放裁剪图像 以裁剪大小保存
this.cropperSettings.keepAspect = false;  // 不保持裁剪图片纵横比
this.cropperSettings.noFileInput = true;  // 不要 input 标签
this.cropperSettings.cropperDrawSettings.strokeWidth = 2;  // 选择框边框宽度
this.cropperSettings.cropperDrawSettings.strokeColor = '#1296db';  // 选择框边框颜色
this.cropperSettings.cropperDrawSettings.fillColor = '#fff';  // 角选择块颜色
this.cropperSettings.markerSizeMultiplier = 1;  // 角选择块大小
this.cropperSettings.canvasWidth = 960;  // 画布宽
this.cropperSettings.canvasHeight = 540;
this.cropperSettings.width = 960;  // 初始选择框的宽
this.cropperSettings.height = 540;
this.data = { image: '' };

以上配置参数与页面样式或保存图片相关,添加了部分注释,点击 get button 对应的代码如下,首先是向主进程取得数据,转换后赋值。

async getScreensht() {let data = await this.electron.ipcRenderer.invoke("get-screenshot");let image_url = data.thumbnail.toDataURL();this.data['image'] = image_url;let image: any = new Image();image.src = image_url;this.cropper.setImage(image);
}

此时页面如下图显示:

在这里插入图片描述

这时拖动四个角可以选择截图区域,拖动中间图标可以移动选择截取的区域,点击 clear 清除页面。

clear() {this.cropper.reset();
}

点击 save button,则会将图片保存,保存图片方法如下,首先是取得截取的数据,再发送到主进程并重置页面。

save() {let base64Data = this.data['image'];if (base64Data) {this.electron.ipcRenderer.send('save-screenshot', {data: base64Data});this.clear();}
}

主进程接收到数据后,处理数据,去除 base64 文件编码信息部分,再通过 fs.writeFileSync() 方法保存本地。

ipcMain.on('save-screenshot', (e, args) => {let temp_file = "C:\\temp\\test.png"; // 文件路径let base64Data = args['data'].replace(/^data:image\/png;base64,/, '');let imageBuffer = Buffer.from(base64Data, 'base64');fs.writeFileSync(temp_file, imageBuffer);
});

到此即可将截屏数据显示再页面上,编辑后保存到本地。不过 ngx-img-cropper 这个插件的功能较少,暂时只能编辑大小。
CropperSettings 还有一些其他的参数,可以看 ngx-img-cropper 教程,centerTouchRadius 可以设置拖动图标的范围,默认是图标所在区域的一小部分。
一些问题,如果编辑图片的窗口是动态的,则 this.cropperSettings.canvasWidth = 960; 这些设置宽高的参数可以在 ngOnInit() 初始化中取得参数后设置。
当前截图类似与 QQ 聊天窗口中的屏幕截图按钮,会将主窗口一同截取。如果想实现 QQ 截图快捷键的操作(不截取聊天窗口,本项目是主窗口),
一种办法是在通过 desktopCapturer.getSources() 取得屏幕资源数据前最小化(minimize 方法)主窗口。并在资源数据返回到渲染进程时,再显示(show 方法)主窗口。
需要注意,要先判断主窗口最小化,再取数据,因为 minimize 需要等待时间才能获取数据。

相关文章:

Electron 应用实现截图并编辑功能

Electron 应用实现截图并编辑功能 Electron 应用如何实现截屏功能&#xff0c;有两种思路&#xff0c;作为一个框架是否可以通过框架实现截屏&#xff0c;另一种就是 javaScript 结合 html 中画布功能实现截屏。 在初步思考之后&#xff0c;本文优先探索使用 Electron 实现截屏…...

前端= 结构(HTML)+ 样式(CSS)+ 行为(JavaScript)

前端开发确实涵盖了行为&#xff08;JavaScript&#xff09;、样式&#xff08;CSS&#xff09;和结构&#xff08;HTML&#xff09;这三个主要方面。这三个方面在前端开发中密切协作&#xff0c;共同构建用户界面和用户体验。 结构&#xff08;Structure&#xff09;&#xff…...

Flink-网络流控及反压剖析

参考&#xff1a; Apache Flink学习网...

redis 和 mongodb 比较

Redis和MongoDB是两种不同类型的数据库&#xff0c;它们在数据存储和查询方式、数据模型以及适用场景等方面有一些明显的区别。下面是Redis和MongoDB之间的一些比较&#xff1a; 数据模型&#xff1a; Redis&#xff1a;Redis是一个键值存储系统&#xff0c;支持多种数据结构如…...

Linux 主函数参数介绍

主函数如下&#xff1a; int main( int argc, char* argv[], char* envp[]) 参数分析如下&#xff1a; (1) argc 参数个数 (2) argv 参数内容&#xff0c;是char*类型&#xff0c;说明传给主函数的内容是一个一个的字符串。 (3) envp 环境变量&#xff0c;传给主函数的也…...

资料分析(三)—— 基期、现期、人口、增长量

基期 基期值 现期值 - 增长量 增长量/增长率 现期值/1&#xff08;间隔)增长率 化除为乘 &#xff1a;当增长率&#xff5c;r| < 5% 时&#xff0c;&#xff0c; 注&#xff1a;当选项首位相同&#xff0c;第二位也相同时&#xff0c;只能用直除 基期和差 (结合选…...

Java 正则表达式【匹配与分组基本原理】

简介 我们一般使用正则表达式是用来处理字符串的&#xff0c;不管是实际的开发中还是我们的算法竞赛中&#xff0c;使用正则表达式绝对可以大大提升我们的效率。 正则表达式&#xff08;regular expression&#xff09;其实就是对字符串进行模式匹配的技术。 快速入门 我们这里…...

ThreadLocal源码解析

使用ThreadLocal可以为每个线程维护一个线程变量&#xff0c;使用场景为线程间隔离&#xff0c;线程内方法共享&#xff1b; 原理&#xff1a; Thread类中有一个实例属性ThreadLocalMap&#xff0c;ThreadLocalMap中存放的是Entry数组&#xff0c;Entry数组是ThreadLocal和Ob…...

RocketMQ 5.1.0 源码详解 | Producer 发送流程

文章目录 初始化DefaultMQProducer实例发送流程DefaultMQProducer#sendDefaultMQProducerImpl#sendMQClientInstance#updateTopicRouteInfoFromNameServer使用特定 topic 获取路由信息使用默认 topic 获取路由信息 DefaultMQProducerImpl#sendDefaultImpl发送流程总结 初始化De…...

电脑ip地址怎么改 ip地址怎么改到别的城市

一、ip地址怎么改到别的城市 1.ip地址怎么改到别的城市&#xff0c;1、重启WIFI路由设备 一般手机或电脑在家或公司上网时都是接入到路由器的WIFI网络,再由路由器分配上网IP地址,如果要更换上网IP那么重启路由器设备后,路由器会向网络运营商进行宽带的重新拨号,此时手机或电脑设…...

Android Studio实现列表展示图片

效果&#xff1a; MainActivity 类 package com.example.tabulation;import android.content.Intent; import android.os.Bundle; import android.view.View;import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; im…...

每天一道leetcode:300. 最长递增子序列(动态规划中等)

今日份题目&#xff1a; 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0,3,1,6,2,2,7] …...

【无监督】2、MAE | 自监督模型提取的图像特征也很能打!(CVPR2022 Oral)

文章目录 一、背景二、方法三、效果 论文&#xff1a;Masked Autoencoders Are Scalable Vision Learners 代码&#xff1a;https://github.com/facebookresearch/mae 出处&#xff1a;CVPR2022 Oral | 何凯明 | FAIR 一、背景 本文的标题突出了两个词&#xff1a; masked…...

pytorch单机多卡后台运行

nohup sh ./train_chat.sh > train_chat20230814.log 2>1&参考资料 Pytorch单机多卡后台运行的解决办法...

linux配置上网 linux adsl拨号上网设置

Linux里面配置ADSL上网是件很麻烦的事。但配置完成之后就能开机自动拨号上网&#xff0c;可谓十分的方便。支持的系统有Redhat,CentOS,SuSE,FreeBSD,Ubuntu等常见的Linux。 工具/原料 ADSL网络&#xff0c;电信&#xff0c;网通&#xff0c;移动等常见宽带。 Linux系统的安装光…...

XML学习基础知识归纳(一)

一、XML基本概述 &#xff08;1&#xff09;概念&#xff1a;XML是可扩展的标记语言&#xff0c;xml文档的后缀名为 .xml &#xff08;2&#xff09;作用&#xff1a;用来用来传输和存储数据&#xff0c;不用于表现和展示数据&#xff0c;这点呢相比于HTML来说是不同的&#…...

2023.8.14论文阅读

文章目录 ESPNet: Efficient Spatial Pyramid of Dilated Convolutions for Semantic Segmentation摘要本文方法实验结果 DeepFusion: Lidar-Camera Deep Fusion for Multi-Modal 3D Object Detection摘要本文方法实验结果 ESPNet: Efficient Spatial Pyramid of Dilated Convo…...

FL Studio for Windows-21.1.0.3713中文直装版功能介绍及系统配置要求

FL Studio 21简称FL水果软件,全称是&#xff1a;Fruity Loops Studio编曲&#xff0c;由于其Logo长的比较像一款水果因此&#xff0c;在大家更多的是喜欢称他为水果萝卜&#xff0c;FL studio21是目前最新的版本&#xff0c;这是一款可以让你的计算机就像是一个全功能的录音室&…...

基于网格变形的二维图像变形算法:C++实现与应用

在计算机图形学中&#xff0c;图像变形是一种常见的技术&#xff0c;它可以改变图像的形状和结构&#xff0c;以满足特定的视觉效果或者应用需求。本文将介绍一种基于网格变形的二维图像变形算法&#xff0c;并使用C进行实现。 一、算法原理 网格变形是一种基于网格的图像变形…...

【数据结构】八大排序详解

&#x1f680; 作者简介&#xff1a;一名在后端领域学习&#xff0c;并渴望能够学有所成的追梦人。 &#x1f40c; 个人主页&#xff1a;蜗牛牛啊 &#x1f525; 系列专栏&#xff1a;&#x1f6f9;数据结构、&#x1f6f4;C &#x1f4d5; 学习格言&#xff1a;博观而约取&…...

云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?

大家好&#xff0c;欢迎来到《云原生核心技术》系列的第七篇&#xff01; 在上一篇&#xff0c;我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在&#xff0c;我们就像一个拥有了一块崭新数字土地的农场主&#xff0c;是时…...

论文浅尝 | 基于判别指令微调生成式大语言模型的知识图谱补全方法(ISWC2024)

笔记整理&#xff1a;刘治强&#xff0c;浙江大学硕士生&#xff0c;研究方向为知识图谱表示学习&#xff0c;大语言模型 论文链接&#xff1a;http://arxiv.org/abs/2407.16127 发表会议&#xff1a;ISWC 2024 1. 动机 传统的知识图谱补全&#xff08;KGC&#xff09;模型通过…...

基于Docker Compose部署Java微服务项目

一. 创建根项目 根项目&#xff08;父项目&#xff09;主要用于依赖管理 一些需要注意的点&#xff1a; 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件&#xff0c;否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...

DBAPI如何优雅的获取单条数据

API如何优雅的获取单条数据 案例一 对于查询类API&#xff0c;查询的是单条数据&#xff0c;比如根据主键ID查询用户信息&#xff0c;sql如下&#xff1a; select id, name, age from user where id #{id}API默认返回的数据格式是多条的&#xff0c;如下&#xff1a; {&qu…...

Spring AI 入门:Java 开发者的生成式 AI 实践之路

一、Spring AI 简介 在人工智能技术快速迭代的今天&#xff0c;Spring AI 作为 Spring 生态系统的新生力量&#xff0c;正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务&#xff08;如 OpenAI、Anthropic&#xff09;的无缝对接&…...

MySQL用户和授权

开放MySQL白名单 可以通过iptables-save命令确认对应客户端ip是否可以访问MySQL服务&#xff1a; test: # iptables-save | grep 3306 -A mp_srv_whitelist -s 172.16.14.102/32 -p tcp -m tcp --dport 3306 -j ACCEPT -A mp_srv_whitelist -s 172.16.4.16/32 -p tcp -m tcp -…...

Typeerror: cannot read properties of undefined (reading ‘XXX‘)

最近需要在离线机器上运行软件&#xff0c;所以得把软件用docker打包起来&#xff0c;大部分功能都没问题&#xff0c;出了一个奇怪的事情。同样的代码&#xff0c;在本机上用vscode可以运行起来&#xff0c;但是打包之后在docker里出现了问题。使用的是dialog组件&#xff0c;…...

Redis的发布订阅模式与专业的 MQ(如 Kafka, RabbitMQ)相比,优缺点是什么?适用于哪些场景?

Redis 的发布订阅&#xff08;Pub/Sub&#xff09;模式与专业的 MQ&#xff08;Message Queue&#xff09;如 Kafka、RabbitMQ 进行比较&#xff0c;核心的权衡点在于&#xff1a;简单与速度 vs. 可靠与功能。 下面我们详细展开对比。 Redis Pub/Sub 的核心特点 它是一个发后…...

Spring是如何解决Bean的循环依赖:三级缓存机制

1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间‌互相持有对方引用‌,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...

【Redis】笔记|第8节|大厂高并发缓存架构实战与优化

缓存架构 代码结构 代码详情 功能点&#xff1a; 多级缓存&#xff0c;先查本地缓存&#xff0c;再查Redis&#xff0c;最后才查数据库热点数据重建逻辑使用分布式锁&#xff0c;二次查询更新缓存采用读写锁提升性能采用Redis的发布订阅机制通知所有实例更新本地缓存适用读多…...