第 6 章:屏幕控制与自动化

本章将探索 Electron 最强大的能力之一——对屏幕和输入设备的控制。从截图、录屏到模拟鼠标键盘操作,你将学会如何让应用"看见"屏幕并"操作"系统,这是构建 RPA(机器人流程自动化)和自动化工具的核心基础。

6.1 截图功能(desktopCapturer)

Electron 内置的 desktopCapturer 模块可以捕获屏幕和窗口的画面,这是实现截图、录屏、屏幕分享的基础。

💡 给前端开发者的建议
desktopCapturer 想象成浏览器的 getDisplayMedia API,但它能捕获整个屏幕(包括其他应用窗口),不受浏览器安全限制。

获取屏幕源列表

main.js / preload.js
const { desktopCapturer } = require('electron');

// 获取所有屏幕和窗口
async function getScreenSources() {
  const sources = await desktopCapturer.getSources({
    types: ['window', 'screen'],  // 捕获窗口和屏幕
    thumbnailSize: { width: 300, height: 300 }  // 缩略图尺寸
  });
  
  return sources.map(source => ({
    id: source.id,
    name: source.name,
    thumbnail: source.thumbnail.toDataURL(),  // 转为 base64 图片
    display_id: source.display_id,
    appIcon: source.appIcon ? source.appIcon.toDataURL() : null
  }));
}

// 在 preload 中暴露给渲染进程
contextBridge.exposeInMainWorld('screenAPI', {
  getSources: () => desktopCapturer.getSources({
    types: ['window', 'screen'],
    thumbnailSize: { width: 300, height: 300 }
  })
});

捕获指定屏幕画面

renderer.js
// 在渲染进程中使用 getUserMedia 获取屏幕流
async function captureScreen(sourceId) {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: false,
      video: {
        mandatory: {
          chromeMediaSource: 'desktop',
          chromeMediaSourceId: sourceId,
          minWidth: 1280,
          maxWidth: 1920,
          minHeight: 720,
          maxHeight: 1080
        }
      }
    });
    
    return stream;
  } catch (err) {
    console.error('屏幕捕获失败:', err);
    throw err;
  }
}

// 将 MediaStream 转为图片
async function streamToImage(stream) {
  const video = document.createElement('video');
  video.srcObject = stream;
  await video.play();
  
  const canvas = document.createElement('canvas');
  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;
  
  const ctx = canvas.getContext('2d');
  ctx.drawImage(video, 0, 0);
  
  // 停止流
  stream.getTracks().forEach(track => track.stop());
  
  return canvas.toDataURL('image/png');
}

完整截图工具实现

ScreenshotTool.vue
<template>
  <div class="screenshot-tool">
    <div class="source-list">
      <div 
        v-for="source in sources" 
        :key="source.id"
        class="source-item"
        :class="{ active: selectedSource?.id === source.id }"
        @click="selectSource(source)"
      >
        <img :src="source.thumbnail" class="source-thumb">
        <span>{{ source.name }}</span>
      </div>
    </div>
    <button @click="takeScreenshot" :disabled="!selectedSource">
      截图
    </button>
    <img v-if="screenshot" :src="screenshot" class="screenshot-preview">
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue';

const sources = ref([]);
const selectedSource = ref(null);
const screenshot = ref('');

onMounted(async () => {
  sources.value = await window.screenAPI.getSources();
});

function selectSource(source) {
  selectedSource.value = source;
}

async function takeScreenshot() {
  if (!selectedSource.value) return;
  
  const stream = await captureScreen(selectedSource.value.id);
  screenshot.value = await streamToImage(stream);
  
  // 保存到文件(通过 IPC 将 base64 数据发送给主进程处理)
  // 注意:当 contextIsolation: true 时,Buffer 在渲染进程中不可用
  // 应通过 IPC 将原始数据传输到主进程,由主进程进行 Buffer 操作和文件写入
  await window.electronAPI.saveScreenshot(screenshot.value);
}
</script>

6.2 模拟鼠标操作(@nut-tree/nut.js)

要实现自动化操作,我们需要控制鼠标和键盘。@nut-tree/nut.js 是一个强大的跨平台自动化库,支持 Windows、macOS 和 Linux。

安装依赖
bash
npm install @nut-tree/nut-js
# 或
npm install robotjs  # 备选方案,但 nut.js 更现代

鼠标基本操作

main.js / worker.js
const { 
  mouse, 
  left, 
  right, 
  straightTo,
  Point,
  Button 
} = require('@nut-tree/nut-js');

// 设置鼠标移动速度
mouse.config.mouseSpeed = 500;  // 像素/秒

// 移动鼠标到指定坐标
async function moveMouse(x, y) {
  await mouse.move(straightTo(new Point(x, y)));
}

// 点击操作
async function mouseClick(x, y, button = left) {
  await moveMouse(x, y);
  await mouse.click(button);
}

// 双击
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 沙箱中,无法直接控制操作系统输入设备。

6.3 模拟键盘输入

main.js / worker.js
const { keyboard, Key } = require('@nut-tree/nut-js');

// 输入文本
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();
}

6.4 获取屏幕信息

了解屏幕的分辨率、缩放比例等信息,对于精确控制鼠标位置和适配不同显示器至关重要。

main.js
const { screen } = require('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 屏幕需要特别注意坐标转换。

6.5 窗口控制

获取系统中所有窗口列表,并控制指定窗口(激活、移动、关闭等),是实现自动化工作流的重要能力。

功能实现方式平台支持
获取窗口列表desktopCapturer + 原生模块全平台
激活窗口nut.js window.focus()全平台
移动窗口nut.js window.move()全平台
调整窗口大小nut.js window.resize()全平台
获取窗口位置nut.js window.getPosition()全平台
windowControl.js
const { 
  getWindows,
  getActiveWindow,
  focus,
  move,
  resize 
} = require('@nut-tree/nut-js').window;

// 获取所有窗口
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.6 OCR 文字识别(Tesseract.js)

OCR(光学字符识别)让应用能够"读懂"屏幕上的文字,这是自动化工具的关键能力。

安装 Tesseract.js
bash
npm install tesseract.js
# 下载中文语言包(训练数据)
# 自动下载,也可手动放置到项目目录
ocr.js
const Tesseract = require('tesseract.js');
const path = require('path');
const fs = require('fs').promises;
// 注意:nut.js 的 screen 对象用于获取鼠标位置和屏幕尺寸,
// 而 Electron 的 screen 模块用于获取显示器详细信息(如 getAllDisplays)
const { screen } = require('@nut-tree/nut-js');
const { screen: electronScreen } = require('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)再识别,全屏识别可能需要几秒时间。

6.7 录屏功能基础

基于 desktopCapturer 和 MediaRecorder API,我们可以实现基础的屏幕录制功能。

ScreenRecorder.vue
<template>
  <div class="screen-recorder">
    <div class="controls">
      <select v-model="selectedSource">
        <option v-for="source in sources" :value="source.id">
          {{ source.name }}
        </option>
      </select>
      <button @click="startRecording" :disabled="isRecording">
        开始录制
      </button>
      <button @click="stopRecording" :disabled="!isRecording">
        停止录制
      </button>
    </div>
    <video v-if="recordedUrl" :src="recordedUrl" controls></video>
  </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.screenAPI.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);
    
    // 保存到文件
    const buffer = Buffer.from(await blob.arrayBuffer());
    await window.electronAPI.saveFile('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>

6.8 实际案例:自动截图 + OCR 识别

让我们整合本章所学,实现一个"自动识别屏幕上指定区域文字"的实用工具:

autoOCR.js
// ===== 注意:AutoOCR 功能需要拆分为主进程和渲染进程两部分 =====
// 以下代码仅为演示逻辑,实际开发时需通过 IPC 桥接两个进程

// ─── 主进程部分 (main.js / ocr-main.js) ───
const { app, desktopCapturer, nativeImage } = require('electron');
const Tesseract = require('tesseract.js');
const fs = require('fs').promises;
const path = require('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:屏幕录制基础

这些能力为下一章的 RPA 桌面助手项目奠定了坚实的技术基础。掌握屏幕控制和自动化,你的应用就拥有了"眼睛"和"双手"。