Skip to content

electron screenshot 截图项目文档

使用 C++获取屏幕图片

运行项目

  1. 环境安装
1. vs2020
2. nodejs
3. node-gyp
  1. 安装模块
npm install
  1. 编译 main.cpp
npm run gyp:configure

npm run gyp:build
  1. 启动 electron
npm start

把 C++代码编译成.node 文件

修改 main.cpp 代码后需要

npm run gyp:rebuild

如果是首次编译 main.cpp

npm run gyp:configure

npm run gyp:build

快捷键

截图 F1

取消(退出截图) ESC

已知问题

  1. 绘制矩形窗口的时候,从下往上绘制,绘制完成后移动窗口可以把矩形移动到窗口以外
  2. 缩放比在 125% 175% 图片会失真的问题 (系统的问题)

c++直接返回 rgba 数据 让 JavaScript 端处理

https://chat.openai.com/c/58ef482d-16f3-49e8-b07d-f3edb95b69c6

性能优化

c++ 返回的图片是 BGRA 格式

如果直接返回需要 JavaScript 进行转换,比较耗时(4k 32 毫秒)

故在 c++端转换 (4k 16 毫秒)

JS
convertBGRAtoRGBA(pixelDataArray);

export default function convertBGRAtoRGBA(buffer) {
  for (let i = 0; i < buffer.length; i += 4) {
    // Swap the red and blue channels
    const temp = buffer[i];
    buffer[i] = buffer[i + 2];
    buffer[i + 2] = temp;
  }
  return buffer;
}
cpp
    GetDIBits(hdcWindow, hbmCapture, 0, height, rgbaData.data(), &bi, DIB_RGB_COLORS);
    auto conversion_start = std::chrono::high_resolution_clock::now();
    // 转换从BGRA到RGBA
    for (size_t i = 0; i < rgbaData.size(); i += 4) {
        std::swap(rgbaData[i], rgbaData[i + 2]);
    }
    auto conversion_end = std::chrono::high_resolution_clock::now();
    log << "Time for [BGRA to RGBA conversion]: " << std::chrono::duration<double, std::milli>(conversion_end - conversion_start).count() << " ms\n";

把 rgba 像素绘制到 canvas 上, 开始使用了 Uint8ClampedArray.from 比较耗时(4k 69 毫秒)

改成 new Uint8ClampedArray(pixelDataArray.buffer) 直接使用 buffer 耗时可以降到到 21 毫秒

js
// const data = Uint8ClampedArray.from(pixelDataArray);
console.time("创建一个Image Data对象");
const data = new Uint8ClampedArray(pixelDataArray.buffer);
// 创建一个Image Data对象
const image = new ImageData(data, primaryMonitor.width, primaryMonitor.height);

// 将图像数据绘制到canvas上
backgroundCtx.putImageData(image, 0, 0);

截图以前是写入文件,耗时 140 毫秒,改成内存共享,无需写入文件

使用 c++插件实现跨进程内存共享插件 shared_memory,优化 140 毫秒,渲染进程读取图片内存依然需要 50 毫秒

截图一开始返回的 bmp 位图文件,改成 rgba 像素返回,canvas 无需转成,直接 putImageData 即可

由 90 毫秒加载图片 优化到 20 毫秒

当缩放比在 1.75 也就是屏幕不可以被缩放比整除的时候会出现屏幕坐标和宽度错误

使用 ffi-napi 调用 win32 获取 屏幕的句柄,然后获取 Rect,算出来 x,y,width,height

使用 ffi-napi 获取屏幕耗时较长 4k 30 毫秒,是同步任务

使用 node.js worker 获取,这里需要 rebuild 一下 ffi-napi 才可以在 worker 使用

当然也可以使用 fork child process 子进程

获取屏幕是截图耗时较长,并且是同步任务 (220 毫秒)

使用 worker 或者 child process 获取

获取桌面所有的窗口是一个 electron api 是异步的,也是耗时任务 (4k 70 毫秒)

Promise.all 跟获取屏幕截图、获取屏幕窗口大小(ffi-napi)、desktopCapturer

放到一个 Promise.all 中,这样子就可以同时做三件事情,减少等待时间

electron 渲染进程读取图片内存 buffer 之后,通过 preload.js contextBridge 返回数据 导致耗时 50 毫秒

  1. 去掉 contextBridge
  2. 设置运行访问 node.js
  3. 代码改成 window.electronAPI = {} 方式直接暴露
js
mainWindow = new BrowserWindow({
  webPreferences: {
    preload: path.join(__dirname, "preload.js"),
    nodeIntegration: true,
    contextIsolation: false,
    sandbox: false,
    webSecurity: false,
    spellcheck: false, // 关闭拼写检查错误
  },
});
js
const { contextBridge, ipcRenderer } = require("electron");

const sharedMemory = require("../shared_memory/build/Release/shared_memory");

// 这里不使用 contextBridge
// 原因是 getImageData 转换需要50~90毫秒
window.electronAPI = {
  getImageData(length) {},
};

// contextBridge.exposeInMainWorld("electronAPI", {
//   getImageData(length) {
// });

共享内存跨进程无法访问,获取到的数据为空,获取共享内存显示操作错误等

  1. 父子进程或者 worker 读写数据,必须先在父进程 调用下面 sharedMemory.createOrOpenSharedMemory 方法
js
sharedMemory.closeSharedMemory();
sharedMemory.createOrOpenSharedMemory(
  Math.ceil(width * scaleFactor * height * scaleFactor * 4)
);

然后在子进程或 worker 调用 sharedMemory.writeToSharedMemory 写入数据

js
const addon = require("../screenshot/build/Release/screenshot");
const sharedMemory = require("../shared_memory/build/Release/shared_memory");

process.on("message", () => {
  console.time("addon.captureScreen");
  const bmpData = addon.captureScreen();
  console.log("bmpData.length", bmpData.length);
  sharedMemory.closeSharedMemory();
  sharedMemory.createOrOpenSharedMemory(bmpData.length);
  sharedMemory.writeToSharedMemory(bmpData);
  console.timeEnd("addon.captureScreen");
  process.send(bmpData.length);
});
  1. 非父子派生进程,非 worker 线程读写,

必须要先 sharedMemory.createOrOpenSharedMemory 创建或者打开内存

然后再读取 数据

读取的数据必须是要和写入的数据大小一致

js
window.electronAPI = {
  getImageData(length) {
    console.time("读取图片完整耗时");
    console.time("创建或者打开内存");
    sharedMemory.createOrOpenSharedMemory(length);
    console.timeEnd("创建或者打开内存");
    console.time("读取内存");
    const imageData = sharedMemory.readFromSharedMemory();
    console.timeEnd("读取内存");
    console.time("关闭内存");
    sharedMemory.closeSharedMemory();
    console.timeEnd("关闭内存");
    console.log("imageData", imageData.length, Date.now());
    console.timeEnd("读取图片完整耗时");
    return imageData;
  },

electron 渲染进程启用 共享内存 SharedArrayBuffer

js
// https://stackoverflow.com/questions/71770369/use-sharearraybuffer-from-an-electron-app
// Enable SharedArrayBuffer
mainWindow.webContents.session.webRequest.onHeadersReceived(
  (details, callback) => {
    details.responseHeaders["Cross-Origin-Opener-Policy"] = ["same-origin"];
    details.responseHeaders["Cross-Origin-Embedder-Policy"] = ["require-corp"];
    callback({ responseHeaders: details.responseHeaders });
  }
);

ffi-napi 在 electron 20.3.8 以上版本(不包含)引入报错

"Error in native callback" using ffi-napi in electron and electron-builder

stack overflow https://stackoverflow.com/questions/75668307/error-in-native-callback-using-ffi-napi-in-electron-and-electron-builder

使用小于等于 20.3.8 版本的 electron

获取桌面任务栏窗口

  1. 使用 Window Detective 获取到任务栏对应的窗口类
  2. 使用 ffi-napi 通过“Window 类”获取到任务栏窗口句柄
  3. 通过窗口句柄遍历所有的子窗口,句柄
  4. 通过 ffi-napi 根据窗口句柄然会窗口坐标 (top left right bottom)
  5. 最后转成 x,y,width,height

使用 Window-Detective-3.5.1-setup.exe