Electron for 鸿蒙PC项目实战案例之气泡图组件

项目概述

本项目是一个基于Electron框架开发的气泡图组件应用,为鸿蒙PC平台提供三维数据可视化功能。气泡图通过点的位置和大小同时展示三个维度的数据信息,适用于展示多维度数据的分布和关联关系。这是Electron初学者学习高级数据可视化的理想示例。

功能特点

  • 气泡图渲染:使用Canvas API绘制精美的气泡图
  • 三维数据可视化:同时展示X、Y坐标和气泡大小三个维度的数据
  • 颜色映射:支持基于数据值的颜色映射功能
  • 交互功能:支持气泡悬停提示、缩放、平移等交互操作
  • 气泡大小映射:支持自定义气泡大小映射规则
  • 网格与坐标轴:支持网格线和坐标轴的自定义配置
  • 数据导入导出:支持CSV格式数据导入和JSON格式导出
  • 响应式设计:适配不同窗口大小的界面布局
  • 鸿蒙PC适配:针对鸿蒙PC平台优化的用户体验

技术架构

核心架构

  • 主进程 (Main Process) :由main.js实现,负责应用程序生命周期管理和窗口创建
  • 渲染进程 (Renderer Process) :由renderer.js实现,负责气泡图渲染和用户交互
  • 预加载脚本 (Preload Script) :由preload.js实现,提供安全的进程间通信桥接

文件结构

复制代码
98-bubble-chart/
├── main.js         # 主进程文件,应用入口
├── preload.js      # 预加载脚本,进程通信
├── index.html      # 应用界面
├── style.css       # 样式文件
├── renderer.js     # 渲染进程逻辑
└── README.md       # 项目说明文档

核心代码解析

主进程 (main.js)

主进程负责创建和管理应用窗口,处理文件系统操作:

javascript 复制代码
const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  // 创建浏览器窗口并配置安全选项
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    title: '气泡图组件 - Electron for 鸿蒙PC项目实战案例',
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      nodeIntegration: false,
      contextIsolation: true,
      sandbox: true
    }
  });
  
  win.loadFile('index.html');
}

// 应用生命周期管理
app.whenReady().then(() => {
  createWindow();
  
  app.on('activate', () => {
    if (BrowserWindow.getAllWindows().length === 0) createWindow();
  });
});

app.on('window-all-closed', () => {
  if (process.platform !== 'darwin') app.quit();
});

渲染进程 (renderer.js)

渲染进程负责气泡图的核心渲染逻辑:

javascript 复制代码
class BubbleChart {
  constructor(canvasId, options = {}) {
    this.canvas = document.getElementById(canvasId);
    this.ctx = this.canvas.getContext('2d');
    this.data = options.data || [];
    this.title = options.title || '气泡图';
    this.xLabel = options.xLabel || 'X轴';
    this.yLabel = options.yLabel || 'Y轴';
    this.zLabel = options.zLabel || '气泡大小';
    this.colors = options.colors || ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6'];
    this.minRadius = options.minRadius || 3;
    this.maxRadius = options.maxRadius || 25;
    this.margin = options.margin || { top: 50, right: 50, bottom: 50, left: 50 };
    this.zoom = 1;
    this.pan = { x: 0, y: 0 };
    this.isDragging = false;
    this.lastMousePos = { x: 0, y: 0 };
    this.tooltip = null;
    this.init();
  }
  
  // 初始化图表
  init() {
    this.calculateDimensions();
    this.calculateScales();
    this.render();
    this.bindEvents();
    this.createTooltip();
  }
  
  // 计算画布尺寸
  calculateDimensions() {
    this.width = this.canvas.width - this.margin.left - this.margin.right;
    this.height = this.canvas.height - this.margin.top - this.margin.bottom;
  }
  
  // 计算坐标轴刻度和气泡大小映射
  calculateScales() {
    if (this.data.length === 0) return;
    
    // 找出数据的最小值和最大值
    let minX = Infinity, maxX = -Infinity;
    let minY = Infinity, maxY = -Infinity;
    let minZ = Infinity, maxZ = -Infinity;
    
    this.data.forEach(dataset => {
      dataset.points.forEach(point => {
        minX = Math.min(minX, point.x);
        maxX = Math.max(maxX, point.x);
        minY = Math.min(minY, point.y);
        maxY = Math.max(maxY, point.y);
        minZ = Math.min(minZ, point.z);
        maxZ = Math.max(maxZ, point.z);
      });
    });
    
    // 添加一些边距
    const xPadding = (maxX - minX) * 0.1 || 1;
    const yPadding = (maxY - minY) * 0.1 || 1;
    
    this.xScale = {
      min: minX - xPadding,
      max: maxX + xPadding,
      range: maxX - minX + 2 * xPadding
    };
    
    this.yScale = {
      min: minY - yPadding,
      max: maxY + yPadding,
      range: maxY - minY + 2 * yPadding
    };
    
    this.zScale = {
      min: minZ,
      max: maxZ,
      range: maxZ - minZ || 1
    };
  }
  
  // 坐标转换
  toCanvasX(x) {
    return this.margin.left + ((x - this.xScale.min) / this.xScale.range * this.width) * this.zoom + this.pan.x;
  }
  
  toCanvasY(y) {
    return this.margin.top + (this.height - (y - this.yScale.min) / this.yScale.range * this.height) * this.zoom + this.pan.y;
  }
  
  // 计算气泡半径
  calculateRadius(z) {
    // 使用平方根映射,使气泡面积与z值成正比
    const zRatio = (z - this.zScale.min) / this.zScale.range;
    const radiusRange = this.maxRadius - this.minRadius;
    return this.minRadius + Math.sqrt(zRatio) * radiusRange;
  }
  
  // 绘制气泡图
  render() {
    // 清空画布
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    
    // 绘制标题
    this.drawTitle();
    
    // 绘制坐标轴标签
    this.drawAxisLabels();
    
    // 绘制坐标轴和网格
    this.drawAxesAndGrid();
    
    // 绘制气泡
    this.data.forEach((dataset, index) => {
      const color = this.colors[index % this.colors.length];
      dataset.points.forEach(point => {
        this.drawBubble(point, color);
      });
    });
    
    // 绘制图例
    this.drawLegend();
  }
  
  // 绘制气泡
  drawBubble(point, color) {
    const x = this.toCanvasX(point.x);
    const y = this.toCanvasY(point.y);
    const radius = this.calculateRadius(point.z);
    
    // 绘制气泡外边框
    this.ctx.beginPath();
    this.ctx.arc(x, y, radius, 0, 2 * Math.PI);
    this.ctx.strokeStyle = '#fff';
    this.ctx.lineWidth = 1;
    this.ctx.stroke();
    
    // 绘制渐变填充
    const gradient = this.ctx.createRadialGradient(
      x - radius * 0.3, y - radius * 0.3, 0,
      x, y, radius
    );
    
    // 解析颜色,创建亮暗变体
    const baseColor = point.color || color;
    const lighterColor = this.lightenColor(baseColor, 20);
    
    gradient.addColorStop(0, lighterColor);
    gradient.addColorStop(1, baseColor);
    
    this.ctx.fillStyle = gradient;
    this.ctx.fill();
  }
  
  // 颜色加亮辅助函数
  lightenColor(color, percent) {
    // 简单实现,将十六进制颜色转为RGB并调整亮度
    let r = parseInt(color.slice(1, 3), 16);
    let g = parseInt(color.slice(3, 5), 16);
    let b = parseInt(color.slice(5, 7), 16);
    
    r = Math.min(255, Math.floor(r * (1 + percent / 100)));
    g = Math.min(255, Math.floor(g * (1 + percent / 100)));
    b = Math.min(255, Math.floor(b * (1 + percent / 100)));
    
    return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
  }
  
  // 绘制坐标轴和网格
  drawAxesAndGrid() {
    this.ctx.save();
    
    // 绘制网格线
    this.ctx.strokeStyle = '#e0e0e0';
    this.ctx.lineWidth = 1;
    
    // 垂直线
    for (let i = 0; i <= 5; i++) {
      const x = this.toCanvasX(this.xScale.min + (this.xScale.range * i / 5));
      this.ctx.beginPath();
      this.ctx.moveTo(x, this.margin.top);
      this.ctx.lineTo(x, this.margin.top + this.height);
      this.ctx.stroke();
      
      // 绘制刻度标签
      this.ctx.fillStyle = '#333';
      this.ctx.textAlign = 'center';
      this.ctx.fillText(
        (this.xScale.min + (this.xScale.range * i / 5)).toFixed(1),
        x,
        this.margin.top + this.height + 20
      );
    }
    
    // 水平线
    for (let i = 0; i <= 5; i++) {
      const y = this.toCanvasY(this.yScale.max - (this.yScale.range * i / 5));
      this.ctx.beginPath();
      this.ctx.moveTo(this.margin.left, y);
      this.ctx.lineTo(this.margin.left + this.width, y);
      this.ctx.stroke();
      
      // 绘制刻度标签
      this.ctx.fillStyle = '#333';
      this.ctx.textAlign = 'right';
      this.ctx.fillText(
        (this.yScale.max - (this.yScale.range * i / 5)).toFixed(1),
        this.margin.left - 10,
        y + 5
      );
    }
    
    // 绘制坐标轴
    this.ctx.strokeStyle = '#333';
    this.ctx.lineWidth = 2;
    
    // X轴
    this.ctx.beginPath();
    this.ctx.moveTo(this.margin.left, this.margin.top + this.height);
    this.ctx.lineTo(this.margin.left + this.width, this.margin.top + this.height);
    this.ctx.stroke();
    
    // Y轴
    this.ctx.beginPath();
    this.ctx.moveTo(this.margin.left, this.margin.top);
    this.ctx.lineTo(this.margin.left, this.margin.top + this.height);
    this.ctx.stroke();
    
    this.ctx.restore();
  }
  
  // 创建提示框
  createTooltip() {
    this.tooltip = document.createElement('div');
    this.tooltip.className = 'bubble-chart-tooltip';
    this.tooltip.style.cssText = `
      position: absolute;
      background: rgba(0, 0, 0, 0.7);
      color: white;
      padding: 8px 12px;
      border-radius: 4px;
      font-size: 12px;
      pointer-events: none;
      z-index: 1000;
      display: none;
    `;
    document.body.appendChild(this.tooltip);
  }
  
  // 显示提示框
  showTooltip(x, y, content) {
    this.tooltip.style.left = x + 'px';
    this.tooltip.style.top = y + 'px';
    this.tooltip.innerHTML = content;
    this.tooltip.style.display = 'block';
  }
  
  // 隐藏提示框
  hideTooltip() {
    this.tooltip.style.display = 'none';
  }
  
  // 查找鼠标位置下的气泡
  findBubbleAtPosition(mouseX, mouseY) {
    const rect = this.canvas.getBoundingClientRect();
    const canvasX = mouseX - rect.left;
    const canvasY = mouseY - rect.top;
    
    // 反向循环,先检查顶层气泡
    for (let datasetIndex = this.data.length - 1; datasetIndex >= 0; datasetIndex--) {
      const dataset = this.data[datasetIndex];
      for (let pointIndex = 0; pointIndex < dataset.points.length; pointIndex++) {
        const point = dataset.points[pointIndex];
        const x = this.toCanvasX(point.x);
        const y = this.toCanvasY(point.y);
        const radius = this.calculateRadius(point.z);
        
        // 计算距离
        const distance = Math.sqrt(
          Math.pow(canvasX - x, 2) + Math.pow(canvasY - y, 2)
        );
        
        if (distance <= radius) {
          return { dataset, point, datasetIndex, pointIndex };
        }
      }
    }
    
    return null;
  }
  
  // 绑定交互事件
  bindEvents() {
    // 鼠标悬停显示提示框
    this.canvas.addEventListener('mousemove', (e) => {
      if (!this.isDragging) {
        const bubble = this.findBubbleAtPosition(e.clientX, e.clientY);
        
        if (bubble) {
          const content = `
            <strong>${bubble.dataset.name}</strong><br>
            ${this.xLabel}: ${bubble.point.x.toFixed(2)}<br>
            ${this.yLabel}: ${bubble.point.y.toFixed(2)}<br>
            ${this.zLabel}: ${bubble.point.z.toFixed(2)}
          `;
          this.showTooltip(e.clientX + 10, e.clientY - 10, content);
          this.canvas.style.cursor = 'pointer';
        } else {
          this.hideTooltip();
          this.canvas.style.cursor = 'default';
        }
      }
    });
    
    // 鼠标离开隐藏提示框
    this.canvas.addEventListener('mouseleave', () => {
      this.hideTooltip();
      this.canvas.style.cursor = 'default';
    });
    
    // 鼠标滚轮缩放
    this.canvas.addEventListener('wheel', (e) => {
      e.preventDefault();
      const rect = this.canvas.getBoundingClientRect();
      const mouseX = e.clientX - rect.left;
      const mouseY = e.clientY - rect.top;
      
      // 根据滚轮方向调整缩放因子
      const scaleFactor = e.deltaY > 0 ? 0.9 : 1.1;
      this.zoom *= scaleFactor;
      
      // 调整平移以保持鼠标位置不变
      this.pan.x = mouseX - (mouseX - this.pan.x) * scaleFactor;
      this.pan.y = mouseY - (mouseY - this.pan.y) * scaleFactor;
      
      this.render();
    });
    
    // 鼠标拖拽平移
    this.canvas.addEventListener('mousedown', (e) => {
      this.isDragging = true;
      const rect = this.canvas.getBoundingClientRect();
      this.lastMousePos = {
        x: e.clientX - rect.left,
        y: e.clientY - rect.top
      };
      this.hideTooltip();
    });
    
    this.canvas.addEventListener('mousemove', (e) => {
      if (this.isDragging) {
        const rect = this.canvas.getBoundingClientRect();
        const currentMousePos = {
          x: e.clientX - rect.left,
          y: e.clientY - rect.top
        };
        
        // 计算拖拽距离
        this.pan.x += currentMousePos.x - this.lastMousePos.x;
        this.pan.y += currentMousePos.y - this.lastMousePos.y;
        
        this.lastMousePos = currentMousePos;
        this.render();
      }
    });
    
    this.canvas.addEventListener('mouseup', () => {
      this.isDragging = false;
    });
    
    this.canvas.addEventListener('mouseleave', () => {
      this.isDragging = false;
    });
  }
  
  // 其他方法:绘制标题、坐标轴标签、图例等
}

// 应用初始化
window.addEventListener('DOMContentLoaded', () => {
  // 生成示例数据
  const generateRandomBubble = () => ({
    x: Math.random() * 100,
    y: Math.random() * 100,
    z: Math.random() * 100 + 10 // 确保气泡大小不为零
  });
  
  const dataset1 = {
    name: '数据集A',
    points: Array.from({ length: 30 }, generateRandomBubble)
  };
  
  const dataset2 = {
    name: '数据集B',
    points: Array.from({ length: 30 }, () => {
      const x = Math.random() * 100;
      const y = x * 0.7 + Math.random() * 20; // 添加一些相关性
      return {
        x: x,
        y: y,
        z: Math.sqrt(x * y) + Math.random() * 20 // 气泡大小基于X和Y的乘积
      };
    })
  };
  
  // 创建气泡图实例
  const bubbleChart = new BubbleChart('bubbleChart', {
    data: [dataset1, dataset2],
    title: '多维度数据分布气泡图',
    xLabel: 'X值',
    yLabel: 'Y值',
    zLabel: '气泡大小',
    minRadius: 5,
    maxRadius: 30
  });
  
  // 绑定控制事件
  document.getElementById('resetViewButton').addEventListener('click', () => {
    bubbleChart.zoom = 1;
    bubbleChart.pan = { x: 0, y: 0 };
    bubbleChart.render();
  });
});

如何运行

  1. 克隆本项目
  2. 安装依赖:npm install
  3. 启动应用:npm start

鸿蒙PC适配改造指南

1. 环境准备

  • 系统要求:Windows 10/11、8GB RAM以上、20GB可用空间

  • 工具安装

    DevEco Studio 5.0+(安装鸿蒙SDK API 20+)

  • Node.js 18.x+

2. 获取Electron鸿蒙编译产物

  1. 登录Electron 鸿蒙官方仓库

  2. 下载Electron 34+版本的Release包(.zip格式)

  3. 解压到项目目录,确认electron/libs/arm64-v8a/下包含核心.so库

3. 部署应用代码

将Electron应用代码按以下目录结构放置:

plaintext 复制代码
web_engine/src/main/resources/resfile/resources/app/
├── main.js
├── package.json
└── src/
    ├── index.html
    ├── preload.js
    ├── renderer.js
    └── style.css

4. 配置与运行

  1. 打开项目:在DevEco Studio中打开ohos_hap目录

  2. 配置签名

    进入File → Project Structure → Signing Configs

  3. 自动生成调试签名或导入已有签名

  4. 连接设备

    启用鸿蒙设备开发者模式和USB调试

  5. 通过USB Type-C连接电脑

  6. 编译运行:点击Run按钮或按Shift+F10

5. 验证检查项

  • ✅ 应用窗口正常显示

  • ✅ 窗口大小可调整,响应式布局生效

  • ✅ 控制台无"SysCap不匹配"或"找不到.so文件"错误

  • ✅ 动画效果正常播放

跨平台兼容性

平台 适配策略 特殊处理
Windows 标准Electron运行 无特殊配置
macOS 标准Electron运行 保留dock图标激活逻辑
Linux 标准Electron运行 确保系统依赖库完整
鸿蒙PC 通过Electron鸿蒙适配层 禁用硬件加速,使用特定目录结构

鸿蒙开发调试技巧

1. 日志查看

在DevEco Studio的Log面板中过滤"Electron"关键词,查看应用运行日志和错误信息。

2. 常见问题解决

  • "SysCap不匹配"错误:检查module.json5中的reqSysCapabilities,只保留必要系统能力

  • "找不到.so文件"错误:确认arm64-v8a目录下四个核心库文件完整

  • 窗口不显示:在main.js中添加app.disableHardwareAcceleration()

  • 动画卡顿:简化CSS动画效果,减少重绘频率

相关推荐
随风一样自由1 小时前
React中实现iframe嵌套登录页面:跨域与状态同步解决方案探讨
javascript·react.js·ecmascript
Chicheng_MA1 小时前
OpenWrt WebUI 交互架构深度解析
javascript·lua·openwrt
IT从业者张某某1 小时前
DAY2-Open Harmony PC 命令行适配指南(Windows版)-Tree命令行工具下载篇
harmonyos
嘴贱欠吻!1 小时前
开源鸿蒙-基于Flutter搭建GitCode口袋工具-2
flutter·华为·开源·harmonyos·gitcode
pale_moonlight1 小时前
九、Spark基础环境实战(下)
大数据·javascript·spark
遇到困难睡大觉哈哈1 小时前
Harmony os——ArkTS 语言笔记(七):注解(Annotation)实战理解
java·笔记·ubuntu·harmonyos·鸿蒙
Aerelin1 小时前
爬虫图片采集(自动化)
开发语言·前端·javascript·爬虫·python·html
Highcharts.js1 小时前
Renko Charts|金融图表之“砖形图”
java·前端·javascript·金融·highcharts·砖型图·砖形图
含若飞1 小时前
列表弹窗实现方案整理
前端·javascript·vue.js