鸿蒙PC 使用 Electron 实现截图功能详解


问题背景

在桌面应用开发中,截图功能是一个常见且重要的需求。无论是用于用户反馈、错误报告、内容分享,还是系统监控和演示,截图功能都能大大提升应用的实用性和用户体验。

 

 

为什么需要截图功能?

  1. 用户反馈和错误报告:用户可以通过截图快速记录问题,帮助开发者快速定位和解决问题

  2. 内容分享:用户可以快速捕获应用界面或屏幕内容,方便分享给他人

  3. 文档制作:在制作教程、文档时,截图是必不可少的工具

  4. 系统监控:对于系统监控类应用,截图功能可以记录系统状态

  5. 演示和展示:在演示应用功能时,截图可以快速保存关键界面

需求分析

  1. 窗口截图:捕获当前应用窗口的完整内容

  2. 屏幕截图:捕获整个屏幕或指定屏幕的内容

  3. 实时预览:捕获后立即显示截图预览

  4. 保存功能:将截图保存为图片文件(PNG格式)

  5. 跨平台兼容:在 Windows、macOS、Linux 以及鸿蒙PC平台上都能正常工作

  6. 高质量输出:确保截图清晰,支持高分辨率显示

技术挑战

  1. 平台差异:不同操作系统的屏幕捕获机制不同

  2. 权限问题:某些平台需要用户授权才能进行屏幕捕获

  3. 性能优化:大分辨率截图可能占用大量内存,需要优化处理

  4. 多屏幕支持:需要支持多显示器环境

  5. 鸿蒙平台适配:需要确保在鸿蒙PC平台上也能正常工作

image-20251124111357685


实现方案

方案对比

方案 优点 缺点 适用场景
webContents.capturePage() 简单可靠,无需权限,捕获窗口内容 只能捕获当前窗口 窗口截图,推荐使用
desktopCapturer API 可捕获屏幕和窗口,功能强大 需要用户授权(某些平台) 屏幕截图,推荐使用
系统命令(screencapture/scrot) 可获取系统级截图 需要解析输出,平台差异大 特殊需求
第三方截图库 功能完整 增加依赖,可能过度设计 复杂场景

最终方案

采用Electron原生API组合方案:

  1. 窗口截图:使用 webContents.capturePage() API

    • 无需用户授权

    • 捕获当前窗口的完整内容

    • 支持高分辨率输出

  2. 屏幕截图:使用 desktopCapturer API

    • 可捕获整个屏幕

    • 支持多显示器

    • 需要用户授权(某些平台)

  3. 数据格式:使用 PNG 格式

    • 无损压缩

    • 支持透明背景

    • 跨平台兼容

  4. 数据传输:使用 base64 编码

    • 便于在进程间传输

    • 可直接用于 <img> 标签显示

    • 易于保存为文件

方案优势

  • 跨平台兼容:使用 Electron 原生 API,支持 Windows、macOS、Linux 和鸿蒙PC平台

  • 无需额外依赖:使用 Electron 内置 API,轻量级

  • 高质量输出:支持高分辨率截图,清晰度好

  • 用户体验好:实时预览,一键保存

  • 功能完整:支持窗口截图和屏幕截图两种模式


代码实现

1. 主进程实现(main.js)

在主进程中实现截图功能的核心逻辑:

const { app, BrowserWindow, ipcMain, powerMonitor, Notification, dialog, desktopCapturer } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
​
// 全局变量:主窗口
let mainWindow = null;
​
// 捕获当前窗口截图
ipcMain.handle('capture-window', async (event) => {
 console.log('收到捕获窗口截图请求');
 
 try {
   if (!mainWindow || mainWindow.isDestroyed()) {
     return { success: false, error: '主窗口不可用' };
  }
   
   // 使用webContents.capturePage()捕获当前窗口
   const image = await mainWindow.webContents.capturePage();
   
   // 转换为PNG格式的Buffer
   const buffer = image.toPNG();
   
   // 转换为base64
   const base64 = buffer.toString('base64');
   const dataUrl = `data:image/png;base64,${base64}`;
   
   console.log('窗口截图已捕获,尺寸:', image.getSize());
   
   return {
     success: true,
     dataUrl: dataUrl,
     width: image.getSize().width,
     height: image.getSize().height
  };
} catch (error) {
   console.error('捕获窗口截图失败:', error);
   return { success: false, error: error.message };
}
});
​
// 获取可用的屏幕源(用于全屏截图)
ipcMain.handle('get-screen-sources', async (event) => {
 console.log('收到获取屏幕源请求');
 
 try {
   const sources = await desktopCapturer.getSources({
     types: ['screen', 'window'],
     thumbnailSize: { width: 0, height: 0 }
  });
   
   console.log('可用的屏幕源数量:', sources.length);
   
   return {
     success: true,
     sources: sources.map(source => ({
       id: source.id,
       name: source.name,
       thumbnail: source.thumbnail.toDataURL()
    }))
  };
} catch (error) {
   console.error('获取屏幕源失败:', error);
   return { success: false, error: error.message };
}
});
​
// 捕获屏幕截图
ipcMain.handle('capture-screen', async (event, sourceId) => {
 console.log('收到捕获屏幕截图请求,源ID:', sourceId);
 
 try {
   // 获取屏幕源,设置较大的thumbnailSize以获取高质量截图
   const sources = await desktopCapturer.getSources({
     types: ['screen'],
     thumbnailSize: { width: 1920, height: 1080 } // 设置较大的尺寸以获取高质量截图
  });
   
   let targetSource = null;
   if (sourceId) {
     targetSource = sources.find(s => s.id === sourceId);
  }
   
   // 如果没有指定源ID或找不到,使用第一个屏幕源
   if (!targetSource && sources.length > 0) {
     targetSource = sources[0];
  }
   
   if (!targetSource) {
     return { success: false, error: '未找到可用的屏幕源' };
  }
   
   // 获取屏幕缩略图(NativeImage对象)
   const thumbnail = targetSource.thumbnail;
   
   // 转换为PNG格式的Buffer,然后转为base64
   const buffer = thumbnail.toPNG();
   const base64 = buffer.toString('base64');
   const dataUrl = `data:image/png;base64,${base64}`;
   
   const size = thumbnail.getSize();
   console.log('屏幕截图已捕获,源:', targetSource.name, '尺寸:', size.width, 'x', size.height);
   
   return {
     success: true,
     dataUrl: dataUrl,
     width: size.width,
     height: size.height,
     sourceName: targetSource.name
  };
} catch (error) {
   console.error('捕获屏幕截图失败:', error);
   return { success: false, error: error.message };
}
});

关键点说明:

  1. 窗口截图

    • 使用 webContents.capturePage() 捕获当前窗口

    • 返回 NativeImage 对象,可转换为 PNG Buffer

    • 无需用户授权,简单可靠

  2. 屏幕截图

    • 使用 desktopCapturer.getSources() 获取屏幕源

    • 设置 thumbnailSize 为较大值(1920x1080)以获取高质量截图

    • 支持多显示器环境

  3. 数据格式

    • 使用 PNG 格式,无损压缩

    • 转换为 base64 编码,便于传输和显示

2. 预加载脚本(preload.js)

在预加载脚本中暴露安全的 API 给渲染进程:

const { contextBridge, ipcRenderer } = require('electron');
​
contextBridge.exposeInMainWorld('electronAPI', {
   // ... 其他API ...
   
   // 捕获当前窗口截图
   captureWindow: () => {
       return ipcRenderer.invoke('capture-window');
  },
   // 获取屏幕源
   getScreenSources: () => {
       return ipcRenderer.invoke('get-screen-sources');
  },
   // 捕获屏幕截图
   captureScreen: (sourceId) => {
       return ipcRenderer.invoke('capture-screen', sourceId);
  },
   // 保存图片(从base64数据)
   saveImage: (base64Data, defaultFileName) => {
       return ipcRenderer.invoke('save-image', base64Data, defaultFileName);
  }
});

关键点说明:

  • 使用 contextBridge 安全地暴露 API

  • 使用 ipcRenderer.invoke() 进行异步通信

  • 所有 API 都返回 Promise,便于使用 async/await

3. 渲染进程实现(index.html)

在渲染进程中实现用户界面和交互逻辑:

3.1 HTML 结构

<!-- 截图按钮 -->
<button οnclick="captureScreenshot()">📸 截图</button>
​
<!-- 截图容器 -->
<div id="screenshot-container" style="display: none; margin-top: 20px; padding: 20px; background: rgba(255, 255, 255, 0.1); border-radius: 10px; color: white; backdrop-filter: blur(10px); max-width: 800px; width: 90%;">
   <h2 style="margin-bottom: 15px;">截图</h2>
   <div id="screenshot-controls" style="margin-bottom: 15px;">
       <button οnclick="captureWindow()" style="margin-right: 10px;">🪟 截取窗口</button>
       <button οnclick="captureScreen()" style="margin-right: 10px;">🖥️ 截取屏幕</button>
       <button οnclick="closeScreenshot()">❌ 关闭</button>
   </div>
   <div id="screenshot-preview" style="margin-top: 15px; display: none;">
       <h3 style="margin-bottom: 10px;">截图预览:</h3>
       <img id="screenshot-image" style="max-width: 100%; border-radius: 8px; border: 2px solid rgba(255, 255, 255, 0.3);">
       <div style="margin-top: 10px; color: rgba(255, 255, 255, 0.8);">
           <div id="screenshot-info"></div>
       </div>
       <div style="margin-top: 10px;">
           <button οnclick="saveScreenshot()" style="margin-right: 10px;">💾 保存截图</button>
           <button οnclick="retakeScreenshot()">📸 重新截图</button>
       </div>
   </div>
   <div id="screenshot-error" style="display: none; color: #ff6b6b; margin-top: 15px;"></div>
</div>

3.2 JavaScript 实现

// 截图相关变量
let currentScreenshotData = null;
let currentScreenshotInfo = null;
​
// 打开截图功能
function captureScreenshot() {
   const screenshotContainer = document.getElementById('screenshot-container');
   screenshotContainer.style.display = 'block';
}
​
// 截取当前窗口
async function captureWindow() {
   console.log('截取当前窗口');
   const screenshotPreview = document.getElementById('screenshot-preview');
   const screenshotImage = document.getElementById('screenshot-image');
   const screenshotInfo = document.getElementById('screenshot-info');
   const screenshotError = document.getElementById('screenshot-error');
   
   screenshotError.style.display = 'none';
   
   try {
       if (window.electronAPI && window.electronAPI.captureWindow) {
           const result = await window.electronAPI.captureWindow();
           
           if (result.success) {
               currentScreenshotData = result.dataUrl;
               currentScreenshotInfo = {
                   type: '窗口',
                   width: result.width,
                   height: result.height
              };
               
               screenshotImage.src = result.dataUrl;
               screenshotInfo.textContent = `类型: 窗口截图 | 尺寸: ${result.width} x ${result.height} 像素`;
               screenshotPreview.style.display = 'block';
               console.log('窗口截图已捕获');
          } else {
               throw new Error(result.error || '截取窗口失败');
          }
      } else {
           throw new Error('截图功能不可用');
      }
  } catch (error) {
       console.error('截取窗口失败:', error);
       screenshotError.style.display = 'block';
       screenshotError.textContent = `截取窗口失败: ${error.message}`;
       screenshotPreview.style.display = 'none';
  }
}
​
// 截取屏幕
async function captureScreen() {
   console.log('截取屏幕');
   const screenshotPreview = document.getElementById('screenshot-preview');
   const screenshotImage = document.getElementById('screenshot-image');
   const screenshotInfo = document.getElementById('screenshot-info');
   const screenshotError = document.getElementById('screenshot-error');
   
   screenshotError.style.display = 'none';
   
   try {
       if (window.electronAPI && window.electronAPI.captureScreen) {
           // 不指定sourceId,使用默认屏幕
           const result = await window.electronAPI.captureScreen(null);
           
           if (result.success) {
               currentScreenshotData = result.dataUrl;
               currentScreenshotInfo = {
                   type: '屏幕',
                   width: result.width,
                   height: result.height,
                   sourceName: result.sourceName
              };
               
               screenshotImage.src = result.dataUrl;
               screenshotInfo.textContent = `类型: 屏幕截图 | 尺寸: ${result.width} x ${result.height} 像素 | 源: ${result.sourceName ||'默认屏幕'}`;
               screenshotPreview.style.display = 'block';
               console.log('屏幕截图已捕获');
          } else {
               throw new Error(result.error || '截取屏幕失败');
          }
      } else {
           throw new Error('截图功能不可用');
      }
  } catch (error) {
       console.error('截取屏幕失败:', error);
       screenshotError.style.display = 'block';
       screenshotError.textContent = `截取屏幕失败: ${error.message}`;
       screenshotPreview.style.display = 'none';
  }
}
​
// 保存截图
async function saveScreenshot() {
   if (!currentScreenshotData) {
       alert('没有可保存的截图');
       return;
  }
   
   try {
       // 生成文件名(带时间戳)
       const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
       const fileName = `screenshot-${timestamp}.png`;
       
       if (window.electronAPI && window.electronAPI.saveImage) {
           const result = await window.electronAPI.saveImage(currentScreenshotData, fileName);
           
           if (!result.canceled && result.filePath) {
               if (result.saved) {
                   alert(`截图已保存到:\n${result.filePath}`);
              } else {
                   alert(`请将截图保存到:\n${result.filePath}`);
              }
          }
      } else {
           // 降级方案:使用浏览器下载
           const link = document.createElement('a');
           link.download = fileName;
           link.href = currentScreenshotData;
           link.click();
           alert('截图已下载');
      }
  } catch (error) {
       console.error('保存截图失败:', error);
       alert('保存截图失败: ' + error.message);
  }
}
​
// 重新截图
function retakeScreenshot() {
   const screenshotPreview = document.getElementById('screenshot-preview');
   screenshotPreview.style.display = 'none';
   currentScreenshotData = null;
   currentScreenshotInfo = null;
   console.log('准备重新截图');
}
​
// 关闭截图
function closeScreenshot() {
   const screenshotContainer = document.getElementById('screenshot-container');
   const screenshotPreview = document.getElementById('screenshot-preview');
   const screenshotError = document.getElementById('screenshot-error');
   
   screenshotContainer.style.display = 'none';
   screenshotPreview.style.display = 'none';
   screenshotError.style.display = 'none';
   currentScreenshotData = null;
   currentScreenshotInfo = null;
   
   console.log('截图功能已关闭');
}

关键点说明:

  1. 状态管理

    • 使用 currentScreenshotData 存储当前截图的 base64 数据

    • 使用 currentScreenshotInfo 存储截图元信息

  2. 错误处理

    • 使用 try-catch 捕获异常

    • 显示友好的错误提示

    • 记录详细的错误日志

  3. 用户体验

    • 实时预览截图

    • 显示截图信息(类型、尺寸等)

    • 支持重新截图和保存


鸿蒙PC适配度注意点

1. 权限配置

在鸿蒙PC平台上,屏幕截图可能需要特殊权限。确保在 module.json5 中配置了必要的权限:

{
 "requestPermissions": [
  {
     "name": "ohos.permission.CAPTURE_SCREEN",
     "reason": "需要屏幕截图权限",
     "usedScene": {
       "abilities": ["EntryAbility"],
       "when": "inuse"
    }
  }
]
}

注意:某些版本的鸿蒙PC可能不需要显式声明此权限,但建议添加以确保兼容性。

2. desktopCapturer API 兼容性

在鸿蒙PC平台上,desktopCapturer API 的行为可能与标准 Electron 略有不同:

  • 权限提示:首次使用屏幕截图功能时,系统可能会弹出权限请求对话框,需要用户授权

  • 屏幕源识别:多显示器环境下,屏幕源的命名和识别方式可能与标准平台不同

  • 缩略图尺寸thumbnailSize 参数的实际效果可能受系统限制

建议

// 在获取屏幕源时,使用较大的thumbnailSize以确保质量
const sources = await desktopCapturer.getSources({
 types: ['screen'],
 thumbnailSize: { width: 1920, height: 1080 } // 根据实际需求调整
});

3. webContents.capturePage() 在鸿蒙PC上的表现

webContents.capturePage() 在鸿蒙PC平台上通常表现良好,但需要注意:

  • 窗口状态:确保窗口已完全加载后再进行截图

  • 透明窗口:如果窗口有透明背景,截图会包含透明区域

  • 性能考虑:大窗口截图可能占用较多内存,建议在截图前检查窗口尺寸

建议

// 在截图前检查窗口状态
if (!mainWindow || mainWindow.isDestroyed()) {
 return { success: false, error: '主窗口不可用' };
}
​
// 可以添加延迟以确保内容完全渲染
await new Promise(resolve => setTimeout(resolve, 100));
const image = await mainWindow.webContents.capturePage();

4. 内存管理

在鸿蒙PC平台上,大分辨率截图可能占用大量内存:

  • 及时清理:截图完成后及时清理不需要的数据

  • 限制尺寸:对于超大窗口,可以考虑限制截图尺寸

  • 流式处理:对于非常大的截图,考虑使用流式处理而不是一次性加载到内存

建议

// 在保存截图后清理内存
async function saveScreenshot() {
   // ... 保存逻辑 ...
   
   // 保存后清理
   if (currentScreenshotData) {
       currentScreenshotData = null;
       currentScreenshotInfo = null;
  }
}

5. 文件保存路径

在鸿蒙PC平台上,文件保存路径可能需要特殊处理:

  • 用户目录:使用系统提供的用户目录,而不是硬编码路径

  • 权限检查:保存前检查是否有写入权限

  • 路径格式:确保路径格式符合鸿蒙系统的要求

建议

// 使用系统提供的用户目录
const { app } = require('electron');
const userDataPath = app.getPath('pictures'); // 或 'documents', 'downloads' 等
const defaultPath = path.join(userDataPath, `screenshot-${Date.now()}.png`);

6. 错误处理

在鸿蒙PC平台上,某些错误可能与标准平台不同:

  • 权限错误:如果用户拒绝权限,会返回特定错误

  • 系统限制:某些系统设置可能限制截图功能

  • 多显示器:多显示器环境下的行为可能不同

建议

// 添加详细的错误处理和日志
try {
   const result = await desktopCapturer.getSources({
       types: ['screen'],
       thumbnailSize: { width: 1920, height: 1080 }
  });
   
   if (result.length === 0) {
       console.warn('未找到可用的屏幕源,可能是权限问题');
       return { success: false, error: '未找到可用的屏幕源,请检查权限设置' };
  }
} catch (error) {
   console.error('获取屏幕源失败:', error);
   
   // 根据错误类型提供不同的提示
   if (error.message.includes('permission')) {
       return { success: false, error: '需要屏幕截图权限,请在系统设置中授权' };
  }
   
   return { success: false, error: error.message };
}

7. 测试建议

在鸿蒙PC平台上测试截图功能时,建议:

  1. 单显示器测试:在单显示器环境下测试基本功能

  2. 多显示器测试:在多显示器环境下测试屏幕源识别

  3. 权限测试:测试权限请求和拒绝场景

  4. 性能测试:测试大分辨率窗口的截图性能

  5. 内存测试:长时间使用后检查内存占用情况

8. 已知问题和解决方案

问题 可能原因 解决方案
屏幕截图返回空白 权限未授权 引导用户在系统设置中授权
截图质量较低 thumbnailSize 设置过小 增大 thumbnailSize 值
多显示器识别错误 屏幕源ID不稳定 使用屏幕名称而非ID进行匹配
内存占用过高 大分辨率截图 限制最大截图尺寸或使用压缩

使用示例

基本使用

// 1. 截取当前窗口
const windowResult = await window.electronAPI.captureWindow();
if (windowResult.success) {
   console.log('窗口截图尺寸:', windowResult.width, 'x', windowResult.height);
   // 显示截图
   document.getElementById('preview').src = windowResult.dataUrl;
}
​
// 2. 截取屏幕
const screenResult = await window.electronAPI.captureScreen(null);
if (screenResult.success) {
   console.log('屏幕截图尺寸:', screenResult.width, 'x', screenResult.height);
   // 显示截图
   document.getElementById('preview').src = screenResult.dataUrl;
}
​
// 3. 保存截图
const saveResult = await window.electronAPI.saveImage(
   windowResult.dataUrl,
   'screenshot.png'
);
if (saveResult.saved) {
   console.log('截图已保存到:', saveResult.filePath);
}

高级使用

// 获取所有可用的屏幕源
const sourcesResult = await window.electronAPI.getScreenSources();
if (sourcesResult.success) {
   sourcesResult.sources.forEach(source => {
       console.log('屏幕源:', source.name, 'ID:', source.id);
  });
   
   // 截取指定的屏幕
   if (sourcesResult.sources.length > 0) {
       const screenResult = await window.electronAPI.captureScreen(
           sourcesResult.sources[0].id
      );
  }
}

总结

本文详细介绍了如何在 Electron 应用中实现截图功能,包括窗口截图和屏幕截图两种模式。实现方案使用 Electron 原生 API,无需额外依赖,具有良好的跨平台兼容性。

核心要点

  1. 窗口截图:使用 webContents.capturePage(),简单可靠,无需权限

  2. 屏幕截图:使用 desktopCapturer API,功能强大,支持多显示器

  3. 数据格式:使用 PNG 格式和 base64 编码,便于传输和保存

  4. 用户体验:实时预览、一键保存、错误提示完善

鸿蒙PC适配要点

  1. 权限配置:确保配置了必要的屏幕截图权限

  2. API兼容性:注意 desktopCapturer 在鸿蒙PC上的行为差异

  3. 内存管理:大分辨率截图需要注意内存占用

  4. 错误处理:针对鸿蒙PC平台的特殊错误进行适配

  5. 测试验证:在多种环境下充分测试

扩展建议

  1. 区域截图:可以扩展支持选择区域进行截图

  2. 截图编辑:添加标注、裁剪等编辑功能

  3. 快捷键支持:添加全局快捷键快速截图

  4. 截图历史:保存截图历史记录

  5. 云存储集成:支持将截图上传到云存储

通过本文的实现方案,您可以在 Electron 应用中快速集成截图功能,并在鸿蒙PC平台上获得良好的用户体验。


参考资料

 

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐