Electron 实现截图工具的整体思路及注意事项

1,381 阅读6分钟

核心技术栈: Electron + Ts + Js + Vite

本文将详细如何借助 Electron 实现一个截图工具的整体思路及注意事项,并不会逐步讲述实现过程,在这里给到代码仓库和实现效果。

https://github.com/1034668900/screen_shot

image.png

Electron

Electron 框架的基础知识这里不再赘述,默认你对 Electron 已经有了一定认识,快速了解可移步Electron 框架核心特性

实现思路

单显示器整体实现思路如下: image.png

目录结构

.
├── README.md
├── dist-electron // vite-plugin-electron 的构建产物
│   ├── main.js
│   └── preload.js
├── electron      // electron 相关代码
│   ├── assets
│   │   ├── close.svg
│   │   ├── download.svg
│   │   └── select.svg
│   ├── captureWindow                // 截图窗口相关内容
│   │   ├── capture.html             // 截图窗口加载的 html
│   │   ├── capture.js               // 截图窗口的 js 入口文件
│   │   ├── captureRender.js         // 封装截图窗口内捕获图像相关的方法
│   │   └── createCaptureWindow.ts   // 封装创建截图窗口相关的逻辑
│   ├── main.ts       // 主进程
│   ├── preload.ts    // 预加载脚本
│   └── utils.ts      // 工具函数
├── electron-builder.json
├── index.html
├── package.json
├── src
│   ├── App.vue       // 截图工具的 UI 界面
│   ├── assets
│   │   ├── base.css
│   │   └── svg
│   ├── components
│   │   ├── Header.vue
│   │   ├── base
│   │   └── icons
│   ├── main.ts
│   ├── router
│   │   └── index.ts
│   └── views
│       └── Home.vue
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.node.json
├── types
│   ├── electron.d.ts // src 内调用 electron 内方法时 ts 校验的声明文件
│   └── env.d.ts
└── vite.config.ts

注意事项

1. 截图窗口创建时使其置顶相关属性不熟悉

在使得截图窗口置顶时可能有部分开发者对这一块儿相关的属性不熟悉,我这里贴上创建截图窗口时的配置属性:

let captureWindow: BrowserWindow = new BrowserWindow({
	frame: false,
	fullscreen: !isDarwin,   // 控制窗口全屏,注意在 mac 下全屏会跳到另一个桌面,因此 mac 下不能使用该属性
	width: screenWidth,
	height: screenHeight,
	x,
	y,
	transparent: true,
	resizable: false,
	movable: false,
	show: false,
	autoHideMenuBar: true,
	enableLargerThanScreen: true,//mac
	skipTaskbar: true,
	alwaysOnTop: true,
	hasShadow: false,
	webPreferences: {
		webSecurity: false,
		nodeIntegration: false,
		contextIsolation: true,
		preload: path.join(__dirname, "preload.js")
	},
});

captureWindow.setOpacity(1);
captureWindow.setAlwaysOnTop(true, "screen-saver");
captureWindow.setFullScreenable(false);
captureWindow.setVisibleOnAllWorkspaces(true);

2. 显示器屏幕资源的来源选择

  • 获取屏幕资源的途径比较多,其中最省事的就是通过 electron 内置模块 desktopCapture 来获取。 image.png

该模块提供了一个 getSources 方法,通过它我们可以获取到当前设备所有屏幕的资源,该接口有==两种玩法==,但效率均不高:

  1. 在配置项里有一个 thumbnailSize 属性,该属性可以获取到当前屏幕的缩略图,这个缩略图我们在拿到后就可以将其作为截图窗口的背景图,但是有一点要注意的是,该接口获取缩略图时本身比较耗时,同时得到的缩略图是 electron 中的 nativeImage 格式,我们在将其转为 DataURL 时也非常耗时,所以通过该接口实现时最终从触发截图到定格画面延时较长。

  2. 该接口还可以捕获屏幕资源返回一个视频流,详情见官方demo。所以另一种玩法就是我们通过从视频流中截取一帧作为背景图,但是该方法我尝试时发现最终得到的分辨率不高,最终就放弃了。

  • 通过三方库来获取屏幕的一帧画面作为背景图。我这里推荐使用:screenshot-desktop。这个三方库同时兼容 linuk、windows 和 macOS,其获取屏幕资源响应很快,electron 内置的接口不光支持获取屏幕缩略图,还支持获取屏幕流,它的实现考虑到的因素非常,可能这也间接导致了我们在仅仅只需要获取一帧画面时通过它来实现显得大材小用。screenshot-desk使用起来非常简单,大家进入到对应的 npm 库即可明白,我这里先讨论一下这个库的实现:
    1. 在 macOS 下,该库的实现借助了 macOS 内置命令行指令 screencapture来实现的,相关源码和screencapture指令相关内容如下:
function all () {
	return new Promise((resolve, reject) => {
		listDisplays()
			.then((displays) => {
				const tmpPaths = displays.map(() => temp.path({ suffix: '.jpg' }))
				exec('screencapture -x -t jpg ' + tmpPaths.join(' '), function (err, stdOut) {
					if (err) {
						return reject(err)
					} else {
						Promise.all(tmpPaths.map(readAndUnlinkP))
						.then(resolve)
						.catch(reject)
					}
				})
			})
	})
}		

image.png 2. 在 windows 下它主要通过 bat 脚本并结合动态库实现的,感兴趣可以去对应源码中翻阅。 3. 接下来我来说明一下该三方库使用过程中一个比较影响项目开发进程的踩坑点screenshot-desktoplistDisplays方法会获取显示器的数量,其中包含每个显示器的 id、label、size、position等信息,但是在不同操作系统上返回的信息不一致,如下。可以看到在 macOS 下,通过该三方库我们拿不到每个显示器的尺寸信息和位置信息,在 windows 下又可以,所在在 macOS 下我们又不得不需要借助 electron 内置的 screen 模块来获取每个显示器信息,但要注意的是 screen模块获取的显示器信息的 ID 和 该三方库获取到的显示器的信息 ID不一致!!! 因此这里需要根据屏幕尺寸和位置信息同步一下两者的 ID 。

// electron 内置 screen 模块获取的显示器信息 - mac
[
  {
    id: 3,
    label: 'PHL 278B1',
    size: { width: 1920, height: 1080 },
    bounds: { x: 0, y: 0, width: 1920, height: 1080 },
    scaleFactor: 2
  },
  {
    id: 1,
    label: '内建视网膜显示器',
    size: { width: 1728, height: 1117 },
    bounds: { x: 1920, y: 64, width: 1728, height: 1117 },
    scaleFactor: 2
  }
]
// screenshot-desktop 获取的显示器信息 - mac
[
	{ name: 'PHL 278B1', primary: true, id: 0 },
	{ name: 'Color LCD', primary: false, id: 1 }
]

// ----------------------------------------------------------//

// electron 内置 screen 模块获取的显示器信息 - windows
[
  {
    id: 2528732444,
    label: '',
    size: { width: 1536, height: 864 },
    bounds: { x: 0, y: 0, width: 1536, height: 864 },
    scaleFactor: 2.5
  }
]

// screenshot-desktop 获取的显示器信息 - windows
[
  {
    id: '\\\\.\\DISPLAY1',
    name: '\\\\.\\DISPLAY1',
    top: 0,
    right: 3840,
    bottom: 2160,
    left: 0,
    dpiScale: 2.5,
    height: 2160,
    width: 3840
  }
]

3. 性能优化建议

在获取屏幕资源上我们能做的提升非常有限,但是我们在创建截图窗口上可以做做文章!可以在项目启动时就预创建和显示器数量对等的截图窗口,但是先使其隐藏,在截图触发后便可直接将其唤起,每次截图关闭后再次进行预创建,只有当整个程序退出时才全部释放。预创建带来的资源消耗是很少的,但是其提升却比较显著。

4. 多显示器模式下唤起不同步

如果你已经完成了在多显示器下的截图实现,并你遇到了在截图触发后,显示器上遮罩定格的时序不一致,那此时你可以在每个截图窗口资源获取完毕后先不着急进行渲染,而是先向主进程通信表示该窗口已经准备显示窗体,然后在主进程判断已准备好显示窗体的数量和显示器数量是否一致,一致时再统一通知个显示器开始显示窗体,即可实现差距较小的同步渲染。

5. Windows 下屏幕缩放问题

在 windows 下屏幕支持缩放,但是通过 electron 的 screen 模块获取的 scaleFactor 缩放因子是屏幕的最大缩放倍数,不会根据屏幕的缩放而变化,因此需要根据 screenshot-desktop 三方库的 dpiScale 来调整缩放因子,并且需要监听缩放倍数的变化动态调整,不然获取到的图像内容在后续截图过程中会因为缩放因子出现尺寸偏差。

感谢你的耐心阅读,如有任何错误欢迎指正交流。