主题切换
第 7 章:屏幕控制与输入模拟
本章将探索 Electron 构建 RPA 应用的核心能力——对屏幕和输入设备的控制。从截图、录屏到模拟鼠标键盘操作,你将学会如何让应用"看见"屏幕并"操作"系统,这是构建 RPA(机器人流程自动化)和自动化工具的核心基础。
自动化技术原理概览
在动手写代码之前,理解自动化技术如何在操作系统层面工作,能帮助你避开许多实际开发中的坑。
OS 输入栈:你的代码如何"触碰"操作系统
当我们谈论"模拟鼠标键盘"时,实际上是在编写代码向操作系统注入输入事件。理解这条链路能帮你更好地诊断"为什么鼠标移过去了但按钮没反应"这类问题:
text
┌─────────────────────────────────────────────┐
│ 应用层 │
│ ┌─────────────────────────────────────┐ │
│ │ nut.js / robotjs / pyautogui │ │
│ │ (跨平台抽象,统一 API) │ │
│ └──────────────┬──────────────────────┘ │
│ │ native addon (node-ffi) │
│ ┌──────────────▼──────────────────────┐ │
│ │ OS API 层 │ │
│ │ Win32 SendInput / X11 XTest / │ │
│ │ macOS CGEventPost │ │
│ └──────────────┬──────────────────────┘ │
│ │ 系统调用 (syscall) │
│ ┌──────────────▼──────────────────────┐ │
│ │ 内核输入子系统 │ │
│ │ (事件队列、权限校验、驱动分发) │ │
│ └──────────────┬──────────────────────┘ │
│ │ HID 协议 │
│ ┌──────────────▼──────────────────────┐ │
│ │ 硬件层 (物理鼠标/键盘设备) │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘@nut-tree-fork/nut-js 承担的角色是跨平台抽象层——它在三个主流操作系统上使用不同的底层 API,但对开发者暴露统一的 JavaScript 接口:
| 操作系统 | 底层输入 API | 备注 |
|---|---|---|
| Windows | SendInput (user32.dll) | 最成熟,无需额外权限 |
| macOS | CGEventPost (CoreGraphics) | 需要辅助功能权限(安全策略) |
| Linux | XTestFakeInput (X11) / libei (Wayland) | 需安装 libXtst;Wayland 限制更严格 |
给前端开发者的建议
把 nut.js 想象成一个跨平台的 fetch()——它封装了三种不同"网络协议"(OS API),对上层提供统一的 Promise API。你在写 mouse.move() 时不需要关心底层是 SendInput 还是 CGEventPost,就像你不需要手动处理 TCP 连接。
为什么输入模拟必须在主进程
这是一个许多初学者踩过的坑。nut.js 的鼠标和键盘操作不能直接在渲染进程中调用。原因在于:
- Chromium 沙箱模型:渲染进程运行在受限的沙箱环境中,它被设计为只能操作 DOM,不能向操作系统发起系统调用
- OS 安全边界:操作系统只信任特权进程注入输入事件——在 Windows 上这需要
SendInput调用来自一个拥有适当权限的用户态进程;在 macOS 上需要辅助功能权限授权 - 进程隔离:如果渲染进程崩溃(这在复杂 Web 应用中并不罕见),不会影响到正在执行的自动化任务
实践建议
如果自动化任务非常耗时(比如循环执行几百个操作),可以考虑使用 Worker 线程(worker_threads)来运行 nut.js 代码,避免阻塞主进程的事件循环。Worker 线程拥有独立的 V8 实例,可以与主进程通过 parentPort.postMessage() 通信。
进阶-1 截图功能(desktopCapturer)
Electron 内置的 desktopCapturer 模块可以捕获屏幕和窗口的画面,这是实现截图、录屏、屏幕分享的基础。
💡 给前端开发者的建议
把 desktopCapturer 想象成浏览器的 getDisplayMedia API,但它能捕获整个屏幕(包括其他应用窗口),不受浏览器安全限制。
获取屏幕源
javascript
import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('screenAPI', {
getSources: () => ipcRenderer.invoke('screen:getSources'),
captureScreen: (sourceId) => ipcRenderer.invoke('screen:capture', sourceId),
saveScreenshot: (base64Data) => ipcRenderer.invoke('screen:saveScreenshot', base64Data),
})desktopCapturer 的底层原理
desktopCapturer 之所以能捕获整个屏幕(而不只是当前浏览器窗口),是因为它绕过了浏览器的安全限制,直接与操作系统的屏幕捕获 API 交互。理解这个机制有助于你排查黑屏、花屏和性能问题。
text
┌───────────────────────────────────────┐
│ Chromium 渲染引擎 │
│ ┌─────────────────────────────┐ │
│ │ Blink / Skia 合成引擎 │ │
│ │ (渲染 HTML 到屏幕缓冲区) │ │
│ └─────────┬───────────────────┘ │
│ │ 写入 │
│ ┌─────────▼───────────────────┐ │
│ │ 操作系统帧缓冲 (Framebuffer)│◄─────│── desktopCapturer
│ │ 包含所有窗口的最终画面 │ │ 从这里读取
│ └─────────────────────────────┘ │
└───────────────────────────────────────┘各平台的底层实现:
| 平台 | 捕获 API | 特点 |
|---|---|---|
| Windows | DXGI Desktop Duplication API (DirectX) | 高性能,直接从 GPU 显存读取,几乎零拷贝 |
| macOS | CGDisplay / CGWindow API (CoreGraphics) | 10.15 起需要屏幕录制权限;支持 Retina 分辨率 |
| Linux | PipeWire (现代) / X11 SHM (传统) | PipeWire 更安全;X11 下可绕过限制捕获任何窗口 |
给前端开发者的建议
desktopCapturer 和浏览器中的 getDisplayMedia() 使用相同的底层捕获机制。关键区别是:浏览器版本只能捕获用户手动选择的标签页或窗口,而 Electron 版本可以枚举并捕获任何屏幕和窗口——这正是 RPA 应用需要的"全能视角"。
性能考量:desktopCapturer 返回的是 NativeImage 对象,如果频繁截图(比如每秒 30 帧做实时 OCR),会产生大量 GPU→CPU 的内存拷贝。对于这种场景,建议:
- 使用
thumbnail.toPNG()的scaleFactor参数缩小截图(0.5 倍即可减少 75% 像素量) - 先裁剪 ROI 再截图,而不是先截全屏再裁剪——前者利用 GPU 硬件缩放
Electron 42 新增:getSources() 的 fetchWindowIcons 选项(Electron 34+)可以获取窗口图标,帮助 RPA 应用在已知目标窗口标题的情况下快速识别目标窗口:
javascript
import { desktopCapturer, nativeImage } from 'electron'
async function getWindowSourcesWithIcons() {
const sources = await desktopCapturer.getSources({
types: ['window'],
thumbnailSize: { width: 300, height: 300 },
// Electron 34+:获取窗口图标,用于 UI 元素识别
fetchWindowIcons: true,
})
return sources.map(source => ({
id: source.id,
name: source.name,
thumbnail: source.thumbnail.toDataURL(),
// appIcon 在 fetchWindowIcons: true 时返回 NativeImage
appIcon: source.appIcon ? source.appIcon.toDataURL() : null,
}))
}进阶-2 模拟鼠标操作
要实现自动化操作,我们需要控制鼠标和键盘。@nut-tree/nut-js 曾是最流行的跨平台自动化库,但原包已转向商业授权并从 npm 下架。目前有两个可行的替代方案:
原包已下架
@nut-tree/nut-js 已从 npm 移除(404 Not Found),无法再通过 npm install 安装。如果你看到旧教程中引用此包,请使用下面的替代方案。
方案 A(推荐):社区 Fork @nut-tree-fork/nut-js
这是原 @nut-tree/nut-js v4.x 的社区维护分支,API 与原库完全一致,现有代码无需改动即可迁移。
bash
npm install @nut-tree-fork/nut-js版本锁定建议
@nut-tree-fork/nut-js 依赖原生模块,版本变化可能导致 API 不兼容。建议锁定主版本(如 "@nut-tree-fork/nut-js": "~4.2.0"),并在升级后运行 @electron/rebuild。
鼠标基本操作
javascript
import {
mouse,
left,
right,
straightTo,
Point,
Button
} from '@nut-tree-fork/nut-js'
// nut.js v4+:若打包报 ESM 错误,请在 electron.vite.config.ts 中 externalize
// 设置鼠标移动速度
mouse.config.mouseSpeed = 500; // 像素/秒
// 移动鼠标到指定坐标
async function moveMouse(x, y) {
await mouse.move(straightTo(new Point(x, y)));
}
// 点击操作(含错误处理)
async function mouseClick(x, y, button = left) {
try {
await moveMouse(x, y);
await mouse.click(button);
} catch (err) {
console.error(`鼠标点击 (${x}, ${y}) 失败:`, err.message);
throw err;
}
}
// 双击
async function mouseDoubleClick(x, y) {
await moveMouse(x, y);
await mouse.doubleClick(left);
}
// 拖拽
async function mouseDrag(fromX, fromY, toX, toY) {
await mouse.move(straightTo(new Point(fromX, fromY)));
await mouse.pressButton(left);
await mouse.move(straightTo(new Point(toX, toY)));
await mouse.releaseButton(left);
}
// 滚动
async function mouseScroll(amount) {
await mouse.scrollDown(amount); // 向下滚动
// await mouse.scrollUp(amount); // 向上滚动
}
// 获取当前鼠标位置
async function getMousePosition() {
return await mouse.getPosition();
}重要提示
鼠标自动化操作需要在主进程或工作线程中执行,不能在渲染进程中直接调用。因为渲染进程运行在 Chromium 沙箱中,无法直接控制操作系统输入设备。
平台兼容性说明
| 功能 | Windows | macOS | Linux |
|---|---|---|---|
| 鼠标控制 | 无需额外权限 | 需要辅助功能权限 | 需要 libXtst 依赖 |
| 键盘模拟 | 无需额外权限 | 同鼠标控制权限 | 需要 libXtst 依赖 |
| 屏幕捕获 | 无需额外权限 | macOS 10.15+ 需要屏幕录制权限 | 需要 libX11 依赖 |
| 窗口控制 | 支持 | 部分功能受限 | 支持 |
macOS 权限提示
首次使用自动化功能时,macOS 会弹出权限请求对话框。用户必须在 系统设置 → 隐私与安全性 → 辅助功能 中手动授权,才能使用 nut.js 的鼠标和键盘控制功能。如果用户拒绝,自动化 API 将抛出异常。建议在应用启动时检查权限状态并引导用户开启。
Linux 依赖安装
bash
# Ubuntu/Debian
sudo apt install libxtst-dev libpng++-dev
# Fedora
sudo dnf install libXtst-devel libpng-devel输入模拟的完整管线:从 JavaScript 到内核
当你调用 mouse.move() 时,背后发生了多层次的系统调用。理解这条链路有助于你判断"自动化不生效"是代码问题还是系统权限问题:
text
┌──────────────────────────────────────────────────┐
│ JavaScript 层 (主进程/Worker) │
│ mouse.move(straightTo(new Point(500, 300))) │
│ │ │
│ ▼ │
│ nut.js 核心层 (TypeScript → 路由到平台实现) │
│ @nut-tree-fork/nut-js/dist/lib/mouse.class.js │
│ │ │
│ ▼ │
│ 原生插件层 (node-ffi / napi-rs / C++ addon) │
│ 通过 FFI 或 N-API 调用动态链接库 (.dll/.dylib/.so)│
│ │ │
│ ▼ │
│ OS 系统调用层 │
│ user32.dll!SendInput (Windows) │
│ CoreGraphics!CGEventPost (macOS) │
│ libXtst!XTestFakeInput (Linux X11) │
│ libei!ei_device_input (Linux Wayland) │
│ │ │
│ ▼ │
│ 内核输入子系统 → HID 驱动 → 合成输入事件 │
└──────────────────────────────────────────────────┘平台输入机制的核心差异:
| 维度 | Windows SendInput | macOS CGEvent | Linux XTest |
|---|---|---|---|
| 权限要求 | 无需提升权限 | 辅助功能权限(系统偏好设置授权) | 无(但需要 X11 访问权限) |
| 合成事件标记 | LLMHF_INJECTED 标志 | kCGEventSourceStatePrivate | 无显式标记 |
| 目标应用能否区分 | 可以(通过低级鼠标钩子检测注入标志) | 部分可以(通过事件来源 API) | 通常无法区分 |
| 与真实设备竞争 | 合成事件排在物理事件之后 | 合成事件与物理事件平等竞态 | 合成事件先于物理事件 |
macOS 辅助功能权限的底层原因:macOS 的安全模型要求任何控制其他应用 UI 的操作都需要用户明确授权。这是因为自动化 API (CGEventPost) 可以绕过应用的输入验证,理论上可以被恶意软件利用。当用户在"系统设置 → 隐私与安全性 → 辅助功能"中授权你的应用后,系统会将你的应用二进制文件签名注册到受信列表中 (TCC.db),之后 CGEventPost 调用才能成功。
合成输入 vs 真实输入
某些应用(尤其游戏和金融软件)会检测输入事件是否为"合成"(程序模拟)。在 Windows 上,通过 SendInput 发送的事件会被标记为 LLMHF_INJECTED,目标应用可以通过低级鼠标钩子 (SetWindowsHookEx(WH_MOUSE_LL)) 检测并拒绝。对于这类场景,可以考虑使用硬件模拟设备(如 Arduino-based HID 模拟器)。
进阶-3 模拟键盘输入
javascript
import { keyboard, Key } from '@nut-tree-fork/nut-js'
// nut.js v4+:若打包报 ESM 错误,请在 electron.vite.config.ts 中 externalize
// 输入文本
async function typeText(text) {
await keyboard.type(text);
}
// 按下单个按键
async function pressKey(key) {
await keyboard.pressKey(key);
await keyboard.releaseKey(key);
}
// 组合键
async function hotkey(keys) {
await keyboard.pressKey(...keys);
await keyboard.releaseKey(...keys);
}
// 常用快捷键示例
async function copy() {
await hotkey([Key.LeftControl, Key.C]); // Windows/Linux
// await hotkey([Key.LeftCommand, Key.C]); // macOS
}
async function paste() {
await hotkey([Key.LeftControl, Key.V]);
}
async function selectAll() {
await hotkey([Key.LeftControl, Key.A]);
}
async function pressEnter() {
await pressKey(Key.Enter);
}
async function pressTab() {
await pressKey(Key.Tab);
}
// 实际应用:自动填写表单
async function fillForm(data) {
await typeText(data.name);
await pressTab();
await typeText(data.email);
await pressTab();
await typeText(data.phone);
await pressTab();
await pressEnter();
}方案 B(备选):RobotJS
如果你偏好更轻量的方案,或者社区 fork 在你的平台上有兼容性问题,可以考虑 robotjs。它是一个老牌桌面自动化库,API 更简单直接,但功能相对基础。
bash
npm install robotjs关键差异:
| 特性 | @nut-tree-fork/nut-js | robotjs |
|---|---|---|
| API 风格 | 异步 Promise | 同步调用 |
| 图像识别 | 支持(模板匹配) | 不支持 |
| 窗口管理 | 支持 | 不支持 |
| 屏幕捕获 | 支持 | 支持(截图) |
| 维护状态 | 社区 fork,周下载 22,000+ | 活跃维护,v0.7.1 |
robotjs 等效代码示例:
javascript
const robot = require('robotjs');
// 设置鼠标移动速度(robotjs 不支持平滑移动,需自行实现)
robot.setMouseDelay(2);
// 移动鼠标到指定坐标
function moveMouse(x, y) {
robot.moveMouse(x, y);
}
// 点击操作
function mouseClick(x, y, button = 'left') {
moveMouse(x, y);
robot.mouseClick(button);
}
// 双击
function mouseDoubleClick(x, y) {
moveMouse(x, y);
robot.mouseClick('left', true); // true = double
}
// 拖拽
function mouseDrag(fromX, fromY, toX, toY) {
moveMouse(fromX, fromY);
robot.mouseToggle('down', 'left');
moveMouse(toX, toY);
robot.mouseToggle('up', 'left');
}
// 滚动
function mouseScroll(amount) {
robot.scrollMouse(0, amount); // x, y
}
// 获取当前鼠标位置
function getMousePosition() {
return robot.getMousePos();
}
// 键盘输入
function typeText(text) {
robot.typeString(text);
}
// 组合键
function hotkey(keys) {
keys.forEach(key => robot.keyToggle(key, 'down'));
keys.reverse().forEach(key => robot.keyToggle(key, 'up'));
}
// 常用快捷键
function copy() {
hotkey(['control', 'c']);
}
function paste() {
hotkey(['control', 'v']);
}重要区别
robotjs 的 API 是同步的,调用后立即执行。这与 @nut-tree-fork/nut-js 的异步 Promise API 不同。在 Electron 主进程中,如果需要执行大量连续操作,建议用 setTimeout 或拆分到 Worker 线程中执行,避免阻塞事件循环。
如何选择
- 需要图像识别、窗口管理、平滑鼠标移动 → 使用
@nut-tree-fork/nut-js - 只需要简单的鼠标键盘控制,且希望依赖更轻量 → 使用
robotjs - 两个库都必须在主进程或 Worker 线程中运行,不能在渲染进程中直接调用
进阶-4 获取屏幕信息
了解屏幕的分辨率、缩放比例等信息,对于精确控制鼠标位置和适配不同显示器至关重要。
javascript
import { screen } from 'electron'
// 获取所有显示器信息
function getDisplayInfo() {
const displays = screen.getAllDisplays();
return displays.map((display, index) => ({
id: display.id,
index: index,
// 物理分辨率
size: {
width: display.size.width,
height: display.size.height
},
// 工作区(排除任务栏/菜单栏)
workArea: {
x: display.workArea.x,
y: display.workArea.y,
width: display.workArea.width,
height: display.workArea.height
},
// 缩放比例(DPI 缩放)
scaleFactor: display.scaleFactor,
// 是否为主显示器
isPrimary: display.isPrimary,
// 旋转角度
rotation: display.rotation,
// 触控支持
touchSupport: display.touchSupport,
// 实际可用分辨率(考虑缩放)
logicalSize: {
width: display.size.width / display.scaleFactor,
height: display.size.height / display.scaleFactor
}
}));
}
// 获取当前鼠标所在的显示器
function getDisplayAtCursor() {
const point = screen.getCursorScreenPoint();
return screen.getDisplayNearestPoint(point);
}
// 获取主显示器
function getPrimaryDisplay() {
return screen.getPrimaryDisplay();
}
// 监听显示器变化
screen.on('display-added', (event, display) => {
console.log('新显示器连接:', display.id);
});
screen.on('display-removed', (event, display) => {
console.log('显示器断开:', display.id);
});
screen.on('display-metrics-changed', (event, display, changedMetrics) => {
console.log('显示器参数变化:', changedMetrics);
});缩放比例处理
Windows 和 macOS 都支持 DPI 缩放(如 125%、150%)。scaleFactor 告诉你当前缩放比例,nut.js 的坐标是物理像素,所以高 DPI 屏幕需要特别注意坐标转换。
进阶-5 窗口控制
获取系统中所有窗口列表,并控制指定窗口(激活、移动、关闭等),是实现自动化工作流的重要能力。
| 功能 | 实现方式 | 平台支持 |
|---|---|---|
| 获取窗口列表 | desktopCapturer + 原生模块 | 全平台 |
| 激活窗口 | nut.js window.focus() | 全平台 |
| 移动窗口 | nut.js window.move() | 全平台 |
| 调整窗口大小 | nut.js window.resize() | 全平台 |
| 获取窗口位置 | nut.js window.getPosition() | 全平台 |
javascript
import { window as nutWindow } from '@nut-tree-fork/nut-js'
const {
getWindows,
getActiveWindow,
focus,
move,
resize
} = nutWindow;
// 获取所有窗口
async function listWindows() {
const windows = await getWindows();
return Promise.all(windows.map(async (win) => {
const title = await win.title;
const region = await win.region;
return {
title,
x: region.left,
y: region.top,
width: region.width,
height: region.height
};
}));
}
// 根据标题查找并激活窗口
async function activateWindow(titlePattern) {
const windows = await getWindows();
for (const win of windows) {
const title = await win.title;
if (title.includes(titlePattern)) {
await focus(win);
return true;
}
}
return false;
}
// 移动窗口到指定位置
async function moveWindow(title, x, y) {
const windows = await getWindows();
for (const win of windows) {
const winTitle = await win.title;
if (winTitle.includes(title)) {
await move(win, { x, y });
return true;
}
}
return false;
}进阶-6 OCR 文字识别(Tesseract.js)
OCR(光学字符识别)让应用能够"读懂"屏幕上的文字,这是自动化工具的关键能力。
OCR 工作原理浅析
Tesseract.js 是 Google Tesseract OCR 引擎的 WebAssembly 移植版本。理解它的内部管线能帮你优化识别速度和准确率:
text
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 图像预处理 │ → │ 文本行检测 │ → │ 字符分割 │ → │ LSTM 识别 │ → │ 后处理 │
│ (二值化、 │ │ (找出行级 │ │ (将每行拆 │ │ (神经网络 │ │ (语言模型 │
│ 降噪、 │ │ 文本区域) │ │ 为字符) │ │ 分类器) │ │ 纠错) │
│ 纠偏) │ │ │ │ │ │ │ │ │
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘各阶段的技术细节:
- 图像预处理:Tesseract 内部使用基于 Leptonica 库的自适应二值化(Otsu 算法),将彩色图像转为黑白。如果这一步失败(文字与背景对比度太低),后续全部失败
- 文本行检测:通过连通域分析和投影轮廓法找到文本行基线,这一步对倾斜文字非常敏感——如果屏幕文字不是水平排列,识别率会骤降
- LSTM 识别:Tesseract 4.x+ 使用基于 LSTM(长短期记忆)的循环神经网络进行字符识别。LSTM 的优势在于可以利用上下文信息——例如"识别"中的"识"字单独看可能像"认",但结合后面的"别"字就能纠正
- 语言模型后处理:加载的语言包不仅是字符库,还包含基于语料的统计语言模型。它会计算字符序列的概率,自动纠正不合理的结果
给前端开发者的建议
把 Tesseract 的 OCR 想象成一张图片"请求"一个远程文字识别 API——但不同的是它完全本地运行(WASM 或 worker 线程),不会向外部发送数据。这就是为什么不需要网络连接也能做 OCR,而且识别内容不会被上传到第三方服务器。
为什么 ROI 裁剪对 OCR 如此关键:
OCR 是个计算密集任务——每一帧图片的每个像素都要经过上述完整管线处理。一张 1920×1080 的屏幕截图有超过 200 万像素,而 OCR 的时间复杂度近似 O(n²)(由 LSTM 的注意力机制决定)。将截图裁剪到只包含目标文字的 200×50 区域,像素量减少 99.5%,识别时间可以从 3-5 秒降到 0.1-0.3 秒。这是 RPA 应用中"先定位后识别"策略(先用图像匹配找到目标区域,再对该区域做 OCR)背后的原因。
安装 Tesseract.js
bashnpm install tesseract.js # 下载中文语言包(训练数据) # 自动下载,也可手动放置到项目目录
javascript
import Tesseract from 'tesseract.js'
import path from 'path'
import { promises as fs } from 'fs'
// 注意:nut.js 的 screen 对象用于获取鼠标位置和屏幕尺寸,
// 而 Electron 的 screen 模块用于获取显示器详细信息(如 getAllDisplays)
import { screen } from '@nut-tree-fork/nut-js'
// nut.js v4+:若打包报 ESM 错误,请在 electron.vite.config.ts 中 externalize
import { screen as electronScreen } from 'electron'
// 从图片文件识别文字
async function recognizeFromFile(imagePath) {
const result = await Tesseract.recognize(
imagePath,
'chi_sim+eng', // 中文简体 + 英文
{
logger: m => console.log(m), // 进度回调
errorHandler: err => console.error(err)
}
);
return {
text: result.data.text,
confidence: result.data.confidence,
words: result.data.words
};
}
// 从屏幕区域截图并识别
async function recognizeFromScreen(x, y, width, height) {
// 截取屏幕区域
const region = new screen.Region(x, y, width, height);
const image = await screen.capture(region);
// 保存临时图片
const tempPath = path.join(app.getPath('temp'), 'ocr-temp.png');
await image.toFile(tempPath);
// 识别
const result = await recognizeFromFile(tempPath);
// 清理临时文件
await fs.unlink(tempPath);
return result;
}
// 在屏幕上查找指定文字的位置
async function findTextOnScreen(targetText) {
// 截取全屏
const displays = electronScreen.getAllDisplays();
const primary = displays.find(d => d.isPrimary);
const result = await recognizeFromScreen(
0, 0,
primary.size.width,
primary.size.height
);
// 查找目标文字
const foundWords = result.words.filter(word =>
word.text.includes(targetText)
);
return foundWords.map(word => ({
text: word.text,
x: word.bbox.x0,
y: word.bbox.y0,
width: word.bbox.x1 - word.bbox.x0,
height: word.bbox.y1 - word.bbox.y0,
confidence: word.confidence
}));
}性能优化
OCR 识别速度取决于图片大小和文字数量。建议先裁剪出感兴趣区域(ROI)再识别,全屏识别可能需要几秒时间。
进阶-7 录屏功能基础
基于 desktopCapturer 和 MediaRecorder API,我们可以实现基础的屏幕录制功能。
vue
<template>
<div class="screen-recorder">
<h3>屏幕录制</h3>
<select v-model="selectedSource" class="source-select">
<option value="">选择屏幕源</option>
<option v-for="source in sources" :key="source.id" :value="source.id">
{{ source.name }}
</option>
</select>
<div class="controls">
<button @click="startRecording" :disabled="isRecording || !selectedSource">开始录制</button>
<button @click="stopRecording" :disabled="!isRecording">停止录制</button>
</div>
<video v-if="recordedUrl" :src="recordedUrl" controls class="preview" />
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const sources = ref([])
const selectedSource = ref('')
const isRecording = ref(false)
const recordedUrl = ref('')
let mediaRecorder = null
let recordedChunks = []
onMounted(async () => {
sources.value = await window.electronAPI.screen?.getSources?.() || []
})
async function startRecording() {
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: selectedSource.value
}
}
})
recordedChunks = []
mediaRecorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
})
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data)
}
}
mediaRecorder.onstop = async () => {
const blob = new Blob(recordedChunks, { type: 'video/webm' })
recordedUrl.value = URL.createObjectURL(blob)
// 保存到文件(通过 IPC 发送二进制数据到主进程)
const buffer = Buffer.from(await blob.arrayBuffer())
await window.electronAPI.saveRecording?.('recording.webm', buffer)
// 停止所有轨道
stream.getTracks().forEach(track => track.stop())
}
mediaRecorder.start(1000) // 每秒收集一次数据
isRecording.value = true
}
function stopRecording() {
if (mediaRecorder && isRecording.value) {
mediaRecorder.stop()
isRecording.value = false
}
}
</script>
<style scoped>
.screen-recorder {
padding: 16px;
}
.source-select {
width: 100%;
padding: 8px;
margin-bottom: 16px;
}
.controls {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
.controls button {
padding: 8px 24px;
}
.preview {
width: 100%;
max-height: 400px;
}
</style>进阶-8 实际案例:自动截图 + OCR 识别
让我们整合本章所学,实现一个"自动识别屏幕上指定区域文字"的实用工具:
javascript
// ===== 注意:AutoOCR 功能需要拆分为主进程和渲染进程两部分 =====
// 以下代码仅为演示逻辑,实际开发时需通过 IPC 桥接两个进程
// ─── 主进程部分 (main.js / ocr-main.js) ───
import { app, desktopCapturer, nativeImage } from 'electron'
import Tesseract from 'tesseract.js'
import { promises as fs } from 'fs'
import path from 'path'
class AutoOCRMain {
constructor() {
this.language = 'chi_sim+eng';
}
// 识别图片中的文字(在主进程中执行 Tesseract,避免阻塞 UI)
async recognize(imagePath) {
const result = await Tesseract.recognize(imagePath, this.language);
return {
text: result.data.text.trim(),
confidence: result.data.confidence,
words: result.data.words.map(w => ({
text: w.text,
confidence: w.confidence,
bbox: w.bbox
}))
};
}
// 保存截图并识别(主进程负责文件 I/O)
async saveAndRecognize(imageDataUrl) {
const timestamp = Date.now();
// 注意:Buffer 只在主进程/预加载脚本中可用,渲染进程需通过 IPC 传递
const buffer = Buffer.from(imageDataUrl.split(',')[1], 'base64');
const screenshotPath = path.join(app.getPath('temp'), `ocr-${timestamp}.png`);
await fs.writeFile(screenshotPath, buffer);
const result = await this.recognize(screenshotPath);
return { ...result, screenshotPath };
}
}
// ─── 渲染进程部分 (renderer.js / Vue 组件) ───
// 以下代码在渲染进程中运行,使用浏览器 API 截图
class AutoOCRRenderer {
constructor() {
this.language = 'chi_sim+eng';
}
// 截图指定区域(使用 Web API,仅在渲染进程中可用)
async captureRegion(sourceId, x, y, width, height) {
const stream = await navigator.mediaDevices.getUserMedia({
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: sourceId
}
}
});
const video = document.createElement('video');
video.srcObject = stream;
await video.play();
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.drawImage(video, x, y, width, height, 0, 0, width, height);
stream.getTracks().forEach(track => track.stop());
return canvas.toDataURL('image/png');
}
// 截图并通过 IPC 交给主进程识别
async screenshotAndRecognize(sourceId, region) {
console.log('正在截图...');
const imageData = await this.captureRegion(
sourceId, region.x, region.y, region.width, region.height
);
// 通过 IPC 将图片数据发送给主进程进行 OCR 识别和文件保存
const result = await window.electronAPI.ocrRecognize(imageData);
return result;
}
}
// ─── IPC 桥接 (preload.js) ───
// contextBridge.exposeInMainWorld('electronAPI', {
// ocrRecognize: (imageData) => ipcRenderer.invoke('ocr:recognize', imageData)
// });
// ─── 使用示例(渲染进程)───
async function demo() {
const ocr = new AutoOCRRenderer();
// 获取屏幕源(通过 IPC 调用 desktopCapturer)
const sources = await window.electronAPI.getScreenSources();
const primaryScreen = sources[0];
// 识别屏幕左上角 400x200 区域的文字
const result = await ocr.screenshotAndRecognize(
primaryScreen.id,
{ x: 0, y: 0, width: 400, height: 200 }
);
console.log('识别结果:', result.text);
console.log('置信度:', result.confidence);
console.log('截图保存:', result.screenshotPath);
}
module.exports = { AutoOCRMain, AutoOCRRenderer };应用场景
这个自动截图+OCR的组合可以应用于:自动读取发票信息、识别验证码、提取屏幕上的错误信息、自动化数据录入等场景。
专题小结
本专题掌握了 Electron 屏幕控制与自动化的核心能力:
- desktopCapturer:捕获屏幕和窗口画面
- nut.js:模拟鼠标移动、点击、拖拽
- 键盘模拟:输入文本、快捷键组合
- 屏幕信息:分辨率、缩放、多显示器管理
- 窗口控制:获取窗口列表、激活、移动窗口
- Tesseract.js:OCR 文字识别
- MediaRecorder:屏幕录制基础
这些能力为第 10 章的 RPA 桌面助手项目奠定了坚实的技术基础。掌握屏幕控制和自动化,你的应用就拥有了"眼睛"和"双手"。