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 中绑定一个点击事件,向主进程发送一个消息取得录屏的初始数据:

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

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

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

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

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

text 复制代码
 [
  {
    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 找到聚焦的窗口,再根据位置判断当前窗口位于哪个屏幕:

ts 复制代码
// 获取当前窗口所在屏幕
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() 返回的多个数据中找到当前点击的屏幕。

text: 复制代码
{
  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

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

ts 复制代码
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 };;
}

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

ts 复制代码
// 渲染进程
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 由于缩放的原因可能与系统设置的分辨率不一样, 再乘上缩放比 scaleFactor
    let 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;
      }
    }
  }
}

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

text 复制代码
{
  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 标签中:

ts 复制代码
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,用于获取截屏,清除截屏,和保存结果。

html 复制代码
<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 中的内容,

ts 复制代码
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 对应的代码如下,首先是向主进程取得数据,转换后赋值。

ts 复制代码
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 清除页面。

ts 复制代码
clear() {
    this.cropper.reset();
}

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

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

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

ts 复制代码
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 需要等待时间才能获取数据。

相关推荐
闲坐含香咀翠5 小时前
Electron 加载原生模块总崩溃?搞懂这两行配置就够了
前端·electron·客户端
灵魂学者13 小时前
使用 Electron 打包项目构建 .EXE 桌面应用程序(简)
electron·node.js·vue·build·桌面应用程序
天天进步201513 小时前
魔音漫创源码解析:性能优化: Electron 环境下的图片管理与文件系统协议处理优化
javascript·性能优化·electron
三声三视13 小时前
Electron+鸿蒙桌面应用实战:跨平台开发完全指南
electron·harmonyos·鸿蒙·桌面
:mnong1 天前
附图报价系统设计分析5
electron·pdf·vue·cad·dwg·定额
会周易的程序员1 天前
aiDgeScanner 工业设备网络扫描与管理工具
网络·c++·物联网·架构·electron·node.js·iot
CAE虚拟与现实2 天前
前后端调试常用工具大全
前端·后端·vue·react·angular
im_AMBER2 天前
Browser Agent 开发:从浏览器插件到Electron CDP
前端·javascript·架构·electron·agent
还好还好不是吗4 天前
用 Electron + Puppeteer 把视频自动发布变成 AI Agent 可调用的 CLI 工具
electron·开源
薛定猫AI11 天前
【深度解析】Gemma Chat 本地 AI 编程 Agent:Electron + MLX + 开源模型的离线 Vibe Coding 实战
javascript·人工智能·electron