Electron for 鸿蒙PC项目实战案例之简单统计组件

项目概述

本项目是一个基于Electron框架开发的简单统计组件应用,为鸿蒙PC平台提供基础的数据统计和分析功能。这是Electron初学者学习数据处理和分析的理想示例,展示了如何在桌面应用中实现统计计算、数据可视化和交互操作。

功能特点

  • 基础统计计算:支持均值、中位数、标准差、最大值、最小值等基础统计指标
  • 数据分组分析:支持数据分组和分类统计功能
  • 直方图展示:使用Canvas API绘制数据分布直方图
  • 箱线图分析:支持数据分布的箱线图可视化
  • 相关性分析:计算和可视化数据之间的相关性
  • 数据导入导出:支持CSV格式数据导入和统计报告导出
  • 实时计算:数据更新时自动重新计算统计指标
  • 响应式设计:适配不同窗口大小的界面布局
  • 鸿蒙PC适配:针对鸿蒙PC平台优化的用户体验

技术架构

核心架构

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

文件结构

复制代码
99-simple-stats/
├── main.js         # 主进程文件,应用入口
├── preload.js      # 预加载脚本,进程通信
├── index.html      # 应用界面
├── style.css       # 样式文件
├── renderer.js     # 渲染进程逻辑
├── stats.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();
});

统计计算模块 (stats.js)

统计计算模块提供了核心的统计分析功能:

javascript 复制代码
class SimpleStats {
  constructor() {}
  
  // 计算均值
  static mean(data) {
    if (!data || data.length === 0) return 0;
    const sum = data.reduce((acc, val) => acc + val, 0);
    return sum / data.length;
  }
  
  // 计算中位数
  static median(data) {
    if (!data || data.length === 0) return 0;
    const sortedData = [...data].sort((a, b) => a - b);
    const mid = Math.floor(sortedData.length / 2);
    
    if (sortedData.length % 2 === 0) {
      return (sortedData[mid - 1] + sortedData[mid]) / 2;
    } else {
      return sortedData[mid];
    }
  }
  
  // 计算标准差
  static standardDeviation(data) {
    if (!data || data.length === 0) return 0;
    const avg = this.mean(data);
    const squareDiffs = data.map(value => {
      const diff = value - avg;
      return diff * diff;
    });
    const avgSquareDiff = this.mean(squareDiffs);
    return Math.sqrt(avgSquareDiff);
  }
  
  // 计算最小值
  static min(data) {
    if (!data || data.length === 0) return 0;
    return Math.min(...data);
  }
  
  // 计算最大值
  static max(data) {
    if (!data || data.length === 0) return 0;
    return Math.max(...data);
  }
  
  // 计算四分位数
  static quartiles(data) {
    if (!data || data.length === 0) return { q1: 0, q2: 0, q3: 0 };
    const sortedData = [...data].sort((a, b) => a - b);
    const n = sortedData.length;
    
    const q1Index = Math.floor(n * 0.25);
    const q2Index = Math.floor(n * 0.5);
    const q3Index = Math.floor(n * 0.75);
    
    return {
      q1: sortedData[q1Index],
      q2: sortedData[q2Index],
      q3: sortedData[q3Index]
    };
  }
  
  // 计算相关性(皮尔逊相关系数)
  static correlation(x, y) {
    if (!x || !y || x.length !== y.length || x.length === 0) return 0;
    
    const n = x.length;
    const sumX = x.reduce((acc, val) => acc + val, 0);
    const sumY = y.reduce((acc, val) => acc + val, 0);
    const sumXY = x.reduce((acc, val, i) => acc + val * y[i], 0);
    const sumX2 = x.reduce((acc, val) => acc + val * val, 0);
    const sumY2 = y.reduce((acc, val) => acc + val * val, 0);
    
    const numerator = n * sumXY - sumX * sumY;
    const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
    
    return denominator === 0 ? 0 : numerator / denominator;
  }
  
  // 创建直方图数据
  static histogram(data, binCount = 10) {
    if (!data || data.length === 0) return { bins: [], counts: [] };
    
    const min = this.min(data);
    const max = this.max(data);
    const binWidth = (max - min) / binCount;
    
    const bins = [];
    const counts = new Array(binCount).fill(0);
    
    // 创建分箱边界
    for (let i = 0; i <= binCount; i++) {
      bins.push(min + i * binWidth);
    }
    
    // 统计每个分箱的数据数量
    data.forEach(value => {
      if (value === max) {
        counts[binCount - 1]++;
      } else {
        const binIndex = Math.floor((value - min) / binWidth);
        counts[binIndex]++;
      }
    });
    
    return { bins, counts };
  }
  
  // 生成箱线图数据
  static boxPlotData(data) {
    if (!data || data.length === 0) return null;
    
    const min = this.min(data);
    const max = this.max(data);
    const { q1, q2, q3 } = this.quartiles(data);
    
    // 计算四分位距
    const iqr = q3 - q1;
    
    // 计算异常值边界
    const lowerBound = q1 - 1.5 * iqr;
    const upperBound = q3 + 1.5 * iqr;
    
    // 找出异常值
    const outliers = data.filter(value => 
      value < lowerBound || value > upperBound
    );
    
    // 找出非异常值的最小值和最大值
    const nonOutliers = data.filter(value => 
      value >= lowerBound && value <= upperBound
    );
    const whiskerMin = nonOutliers.length > 0 ? Math.min(...nonOutliers) : min;
    const whiskerMax = nonOutliers.length > 0 ? Math.max(...nonOutliers) : max;
    
    return {
      min,
      max,
      q1,
      q2,
      q3,
      iqr,
      lowerBound,
      upperBound,
      whiskerMin,
      whiskerMax,
      outliers
    };
  }
  
  // 数据分组
  static groupBy(data, groupByKey) {
    if (!data || !Array.isArray(data) || data.length === 0) return {};
    
    return data.reduce((groups, item) => {
      const key = item[groupByKey];
      if (!groups[key]) {
        groups[key] = [];
      }
      groups[key].push(item);
      return groups;
    }, {});
  }
}

module.exports = SimpleStats;

渲染进程 (renderer.js)

渲染进程负责UI交互和数据可视化:

javascript 复制代码
// 引入统计计算模块
const SimpleStats = require('./stats.js');

class StatsVisualizer {
  constructor(containerId) {
    this.container = document.getElementById(containerId);
    this.data = [];
    this.init();
  }
  
  // 初始化UI
  init() {
    // 创建数据输入区域
    this.createDataInputArea();
    
    // 创建统计结果展示区域
    this.createStatsDisplayArea();
    
    // 创建可视化区域
    this.createVisualizationArea();
    
    // 生成示例数据
    this.generateSampleData();
    
    // 初始计算和可视化
    this.updateStats();
  }
  
  // 生成示例数据
  generateSampleData() {
    // 生成50个正态分布的随机数
    const normalData = [];
    for (let i = 0; i < 50; i++) {
      // Box-Muller变换生成正态分布
      let u = 0, v = 0;
      while(u === 0) u = Math.random();
      while(v === 0) v = Math.random();
      const z = Math.sqrt(-2.0 * Math.log(u)) * Math.cos(2.0 * Math.PI * v);
      normalData.push(50 + z * 10); // 均值50,标准差10
    }
    
    this.data = normalData;
    this.updateDataInput();
  }
  
  // 更新数据输入区域
  updateDataInput() {
    const dataInput = document.getElementById('dataInput');
    dataInput.value = this.data.join(', ');
  }
  
  // 从输入区域读取数据
  readDataFromInput() {
    const dataInput = document.getElementById('dataInput');
    const rawData = dataInput.value.split(',').map(item => parseFloat(item.trim())).filter(item => !isNaN(item));
    
    if (rawData.length > 0) {
      this.data = rawData;
      return true;
    }
    
    return false;
  }
  
  // 更新统计结果
  updateStats() {
    if (this.data.length === 0) return;
    
    // 计算统计指标
    const mean = SimpleStats.mean(this.data);
    const median = SimpleStats.median(this.data);
    const stdDev = SimpleStats.standardDeviation(this.data);
    const min = SimpleStats.min(this.data);
    const max = SimpleStats.max(this.data);
    const { q1, q2, q3 } = SimpleStats.quartiles(this.data);
    
    // 更新统计结果显示
    document.getElementById('statsMean').textContent = mean.toFixed(2);
    document.getElementById('statsMedian').textContent = median.toFixed(2);
    document.getElementById('statsStdDev').textContent = stdDev.toFixed(2);
    document.getElementById('statsMin').textContent = min.toFixed(2);
    document.getElementById('statsMax').textContent = max.toFixed(2);
    document.getElementById('statsQ1').textContent = q1.toFixed(2);
    document.getElementById('statsQ2').textContent = q2.toFixed(2);
    document.getElementById('statsQ3').textContent = q3.toFixed(2);
    
    // 更新可视化
    this.renderHistogram();
    this.renderBoxPlot();
  }
  
  // 渲染直方图
  renderHistogram() {
    const canvas = document.getElementById('histogramCanvas');
    const ctx = canvas.getContext('2d');
    
    // 清空画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    if (this.data.length === 0) return;
    
    // 获取直方图数据
    const binCount = parseInt(document.getElementById('binCountInput').value) || 10;
    const { bins, counts } = SimpleStats.histogram(this.data, binCount);
    
    // 设置绘图区域
    const margin = { top: 30, right: 30, bottom: 50, left: 50 };
    const width = canvas.width - margin.left - margin.right;
    const height = canvas.height - margin.top - margin.bottom;
    
    // 计算比例
    const xScale = width / binCount;
    const maxCount = Math.max(...counts);
    const yScale = height / maxCount;
    
    // 绘制直方图标题
    ctx.font = '16px Arial';
    ctx.fillStyle = '#333';
    ctx.textAlign = 'center';
    ctx.fillText('数据分布直方图', canvas.width / 2, 20);
    
    // 绘制坐标轴标签
    ctx.font = '12px Arial';
    ctx.fillText('数据值', canvas.width / 2, canvas.height - 10);
    
    // Y轴标签(旋转)
    ctx.save();
    ctx.translate(10, canvas.height / 2);
    ctx.rotate(-Math.PI / 2);
    ctx.fillText('频数', 0, 0);
    ctx.restore();
    
    // 绘制直方图柱
    ctx.fillStyle = '#3498db';
    for (let i = 0; i < binCount; i++) {
      const barWidth = xScale - 2; // 留出间隙
      const barHeight = counts[i] * yScale;
      const x = margin.left + i * xScale;
      const y = canvas.height - margin.bottom - barHeight;
      
      ctx.fillRect(x, y, barWidth, barHeight);
      
      // 绘制刻度标签
      ctx.fillStyle = '#666';
      ctx.textAlign = 'center';
      ctx.fillText(bins[i].toFixed(1), x + xScale / 2, canvas.height - margin.bottom + 15);
    }
  }
  
  // 渲染箱线图
  renderBoxPlot() {
    const canvas = document.getElementById('boxPlotCanvas');
    const ctx = canvas.getContext('2d');
    
    // 清空画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    if (this.data.length === 0) return;
    
    // 获取箱线图数据
    const boxData = SimpleStats.boxPlotData(this.data);
    
    // 设置绘图区域
    const margin = { top: 30, right: 30, bottom: 30, left: 50 };
    const width = canvas.width - margin.left - margin.right;
    const height = canvas.height - margin.top - margin.bottom;
    
    // 计算Y轴比例
    const dataMin = Math.min(boxData.min, boxData.whiskerMin);
    const dataMax = Math.max(boxData.max, boxData.whiskerMax);
    const dataRange = dataMax - dataMin;
    const yScale = height / dataRange;
    const yOffset = y => canvas.height - margin.bottom - (y - dataMin) * yScale;
    
    // 绘制箱线图标题
    ctx.font = '16px Arial';
    ctx.fillStyle = '#333';
    ctx.textAlign = 'center';
    ctx.fillText('数据分布箱线图', canvas.width / 2, 20);
    
    // 绘制Y轴
    ctx.beginPath();
    ctx.moveTo(margin.left, margin.top);
    ctx.lineTo(margin.left, canvas.height - margin.bottom);
    ctx.strokeStyle = '#333';
    ctx.stroke();
    
    // 绘制Y轴刻度
    for (let i = 0; i <= 5; i++) {
      const yValue = dataMin + (dataRange * i / 5);
      const y = yOffset(yValue);
      
      // 刻度线
      ctx.beginPath();
      ctx.moveTo(margin.left - 5, y);
      ctx.lineTo(margin.left, y);
      ctx.stroke();
      
      // 刻度标签
      ctx.fillStyle = '#666';
      ctx.textAlign = 'right';
      ctx.fillText(yValue.toFixed(1), margin.left - 10, y + 5);
    }
    
    // 箱子宽度
    const boxWidth = 100;
    const boxX = canvas.width / 2 - boxWidth / 2;
    
    // 绘制箱子
    ctx.beginPath();
    ctx.rect(boxX, yOffset(boxData.q3), boxWidth, yOffset(boxData.q1) - yOffset(boxData.q3));
    ctx.fillStyle = 'rgba(231, 76, 60, 0.2)';
    ctx.fill();
    ctx.strokeStyle = '#e74c3c';
    ctx.stroke();
    
    // 绘制中位数线
    ctx.beginPath();
    ctx.moveTo(boxX, yOffset(boxData.q2));
    ctx.lineTo(boxX + boxWidth, yOffset(boxData.q2));
    ctx.strokeStyle = '#c0392b';
    ctx.lineWidth = 2;
    ctx.stroke();
    ctx.lineWidth = 1;
    
    // 绘制上下须线
    ctx.beginPath();
    // 上须线
    ctx.moveTo(boxX + boxWidth / 2, yOffset(boxData.q3));
    ctx.lineTo(boxX + boxWidth / 2, yOffset(boxData.whiskerMax));
    // 下须线
    ctx.moveTo(boxX + boxWidth / 2, yOffset(boxData.q1));
    ctx.lineTo(boxX + boxWidth / 2, yOffset(boxData.whiskerMin));
    ctx.strokeStyle = '#e74c3c';
    ctx.stroke();
    
    // 绘制须线端点
    ctx.beginPath();
    // 上须线端点
    ctx.moveTo(boxX + boxWidth / 2 - 10, yOffset(boxData.whiskerMax));
    ctx.lineTo(boxX + boxWidth / 2 + 10, yOffset(boxData.whiskerMax));
    // 下须线端点
    ctx.moveTo(boxX + boxWidth / 2 - 10, yOffset(boxData.whiskerMin));
    ctx.lineTo(boxX + boxWidth / 2 + 10, yOffset(boxData.whiskerMin));
    ctx.strokeStyle = '#e74c3c';
    ctx.stroke();
    
    // 绘制异常值
    ctx.fillStyle = '#c0392b';
    boxData.outliers.forEach(value => {
      ctx.beginPath();
      ctx.arc(boxX + boxWidth / 2, yOffset(value), 5, 0, 2 * Math.PI);
      ctx.fill();
    });
  }
  
  // 创建UI组件
  createDataInputArea() {
    const inputArea = document.createElement('div');
    inputArea.className = 'input-area';
    inputArea.innerHTML = `
      <h3>数据输入</h3>
      <textarea id="dataInput" rows="3" placeholder="请输入数据,用逗号分隔..."></textarea>
      <div class="button-group">
        <button id="updateDataButton">更新数据</button>
        <button id="generateRandomDataButton">生成随机数据</button>
      </div>
    `;
    
    this.container.appendChild(inputArea);
    
    // 绑定事件
    document.getElementById('updateDataButton').addEventListener('click', () => {
      if (this.readDataFromInput()) {
        this.updateStats();
      }
    });
    
    document.getElementById('generateRandomDataButton').addEventListener('click', () => {
      this.generateSampleData();
      this.updateStats();
    });
  }
  
  createStatsDisplayArea() {
    const statsArea = document.createElement('div');
    statsArea.className = 'stats-area';
    statsArea.innerHTML = `
      <h3>统计结果</h3>
      <div class="stats-grid">
        <div class="stat-item"><label>均值:</label> <span id="statsMean">0.00</span></div>
        <div class="stat-item"><label>中位数:</label> <span id="statsMedian">0.00</span></div>
        <div class="stat-item"><label>标准差:</label> <span id="statsStdDev">0.00</span></div>
        <div class="stat-item"><label>最小值:</label> <span id="statsMin">0.00</span></div>
        <div class="stat-item"><label>最大值:</label> <span id="statsMax">0.00</span></div>
        <div class="stat-item"><label>第一四分位数 (Q1):</label> <span id="statsQ1">0.00</span></div>
        <div class="stat-item"><label>第二四分位数 (Q2):</label> <span id="statsQ2">0.00</span></div>
        <div class="stat-item"><label>第三四分位数 (Q3):</label> <span id="statsQ3">0.00</span></div>
      </div>
    `;
    
    this.container.appendChild(statsArea);
  }
  
  createVisualizationArea() {
    const vizArea = document.createElement('div');
    vizArea.className = 'visualization-area';
    vizArea.innerHTML = `
      <h3>数据可视化</h3>
      
      <div class="histogram-container">
        <div class="histogram-controls">
          <label for="binCountInput">直方图分箱数:</label>
          <input type="number" id="binCountInput" min="2" max="50" value="10">
          <button id="updateHistogramButton">更新</button>
        </div>
        <canvas id="histogramCanvas" width="500" height="300"></canvas>
      </div>
      
      <div class="boxplot-container">
        <canvas id="boxPlotCanvas" width="500" height="300"></canvas>
      </div>
    `;
    
    this.container.appendChild(vizArea);
    
    // 绑定事件
    document.getElementById('updateHistogramButton').addEventListener('click', () => {
      this.renderHistogram();
    });
  }
}

// 应用初始化
window.addEventListener('DOMContentLoaded', () => {
  const statsVisualizer = new StatsVisualizer('appContainer');
});

如何运行

  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动画效果,减少重绘频率

相关推荐
奔跑的露西ly1 小时前
【HarmonyOS NEXT】组件化与模块化的理解
华为·harmonyos
北极糊的狐1 小时前
使用 vue-awesome-swiper 实现轮播图(Vue3实现教程)
前端·javascript·vue.js
W.Y.B.G1 小时前
vue3项目中集成高德地图使用示例
前端·javascript·网络
晚霞的不甘1 小时前
Flutter 与开源鸿蒙(OpenHarmony)生态融合:从 UI 渲染到系统级能力调用的全链路开发范式
flutter·开源·harmonyos
花先锋队长1 小时前
华为Mate X7:高级感,从何而来?
科技·华为·智能手机·harmonyos
YAY_tyy1 小时前
基于矩形区域的相机自动定位与飞行控制实现
前端·javascript·3d·cesium
国服第二切图仔1 小时前
Electron for 鸿蒙PC项目实战案例之散点图数据可视化应用
信息可视化·electron·鸿蒙pc
随风一样自由1 小时前
React中实现iframe嵌套登录页面:跨域与状态同步解决方案探讨
javascript·react.js·ecmascript
Chicheng_MA1 小时前
OpenWrt WebUI 交互架构深度解析
javascript·lua·openwrt