Qt + HarmonyOS 图表组件开发实战

📱 项目简介

本文将详细介绍如何使用 Qt QML 的 Canvas API 在 HarmonyOS 平台上开发一套完整的图表组件库。该项目实现了11种常见的图表类型,包括折线图、柱状图、饼图、散点图、雷达图、热力图等,展示了如何在 Qt for HarmonyOS 环境中进行自定义图形绘制。

项目地址: https://gitcode.com/szkygc/HarmonyOs_PC-PGC/tree/main/Chart

效果展示:
请添加图片描述

请添加图片描述

✨ 主要功能

  • 📊 11种图表类型: 折线图、柱状图、饼图、散点图、面积图、雷达图、热力图、堆叠柱状图、日期时间图、动态曲线图等
  • 🎨 Canvas 2D绘制: 使用QML Canvas API实现高性能图形渲染
  • 🔄 实时动态更新: 支持动态图表实时数据更新
  • 📱 响应式布局: 自适应不同屏幕尺寸,支持高DPI缩放
  • 🎯 网格布局展示: 3列网格布局,清晰展示所有图表类型
  • 性能优化: 使用Canvas缓存和高效绘制算法

🛠️ 技术栈

  • 开发框架: Qt 5.x for HarmonyOS
  • 编程语言: QML / JavaScript
  • 图形API: Canvas 2D Context
  • 界面框架: Qt Quick Controls 2
  • 构建工具: CMake
  • 目标平台: HarmonyOS (OpenHarmony) / PC

🏗️ 项目架构

Chart/
├── entry/src/main/
│   ├── cpp/
│   │   ├── main.cpp              # 应用入口
│   │   ├── main.qml              # 主界面(网格布局)
│   │   ├── LineChart.qml         # 折线图组件
│   │   ├── BarChart.qml          # 柱状图组件
│   │   ├── PieChart.qml          # 饼图组件
│   │   ├── ScatterChart.qml      # 散点图组件
│   │   ├── AreaChart.qml         # 面积图组件
│   │   ├── RadarChart.qml        # 雷达图组件
│   │   ├── HeatMapChart.qml      # 热力图组件
│   │   ├── DynamicChart.qml      # 动态曲线图组件
│   │   ├── DateTimeChart.qml     # 日期时间图组件
│   │   ├── StackedBarChart.qml   # 堆叠柱状图组件
│   │   ├── GaugeChart.qml        # 仪表盘组件
│   │   ├── qml.qrc               # Qt资源文件
│   │   └── CMakeLists.txt        # 构建配置
│   └── module.json5              # 模块配置

📝 核心功能实现

1. 主界面布局设计

主界面采用 ApplicationWindow + GridLayout 的方式,实现响应式的图表展示网格。

1.1 窗口配置
ApplicationWindow {
    id: root
    visible: true
    title: "Qt Charts 示例"
  
    // 鸿蒙PC窗口标志:启用最大化、最小化、全屏、关闭按钮
    flags: Qt.Window | 
           Qt.WindowFullscreenButtonHint |
           Qt.WindowTitleHint | 
           Qt.WindowSystemMenuHint | 
           Qt.WindowMinMaxButtonsHint | 
           Qt.WindowCloseButtonHint
  
    // 响应式窗口大小
    width: Math.max(Screen.width * 0.8, 1400)
    height: Math.max(Screen.height * 0.8, 900)
  
    // 最小窗口大小
    minimumWidth: 800
    minimumHeight: 600
  
    // 缩放因子:根据窗口宽度计算
    readonly property real scaleFactor: width > 1500 ? 2.0 : 1.5
}

设计要点

  • 使用 ApplicationWindow 支持PC端窗口操作
  • 响应式尺寸设计,适配不同屏幕
  • 动态缩放因子,确保高DPI显示清晰
1.2 图表数据模型
readonly property var chartList: [
    { title: "实时动态曲线", description: "DynamicChart", source: "DynamicChart.qml" },
    { title: "动态曲线X", description: "DynamicChartX", source: "DynamicChartX.qml" },
    { title: "面积图", description: "AreaChart", source: "AreaChart.qml" },
    { title: "日期时间图", description: "DateTimeChart", source: "DateTimeChart.qml" },
    { title: "折线图", description: "LineChart", source: "LineChart.qml" },
    { title: "堆叠柱状图", description: "StackedBarChart", source: "StackedBarChart.qml" },
    { title: "饼图", description: "PieChart", source: "PieChart.qml" },
    { title: "柱状图", description: "BarChart", source: "BarChart.qml" },
    { title: "散点图", description: "ScatterChart", source: "ScatterChart.qml" },
    { title: "雷达图", description: "RadarChart", source: "RadarChart.qml" },
    { title: "热力图", description: "HeatMapChart", source: "HeatMapChart.qml" }
]
1.3 网格布局实现
GridLayout {
    id: chartGrid
    width: root.width - 24 * scaleFactor
    columns: 3  // 3列布局
    columnSpacing: 12 * scaleFactor
    rowSpacing: 12 * scaleFactor
  
    Repeater {
        model: chartList
      
        Rectangle {
            Layout.preferredWidth: (root.width - 48 * scaleFactor) / 3
            Layout.preferredHeight: 300 * scaleFactor
            color: "#FFFFFF"
            border.color: "#CCCCCC"
            border.width: 1
            radius: 4
            clip: true
          
            Column {
                anchors.fill: parent
                anchors.margins: 8 * scaleFactor
                spacing: 4 * scaleFactor
              
                Text {
                    text: modelData.title
                    font.pixelSize: 14 * scaleFactor
                    font.bold: true
                    horizontalAlignment: Text.AlignHCenter
                }
              
                // Canvas绘制图表
                Canvas {
                    id: chartCanvas
                    width: parent.width
                    height: parent.height - titleHeight
                    // ... 图表绘制逻辑
                }
            }
        }
    }
}

2. Canvas 图表绘制核心

项目使用 QML 的 Canvas 组件和 2D Context API 进行图表绘制,这是实现自定义图表的关键技术。

2.1 Canvas 基础结构
Canvas {
    id: chartCanvas
    width: parent.width
    height: parent.height
  
    property var chartData: []  // 图表数据
    property string chartType: "line"  // 图表类型
  
    onPaint: {
        var ctx = getContext("2d")
        if (!ctx) return
      
        var width = chartCanvas.width
        var height = chartCanvas.height
      
        // 清空画布
        ctx.clearRect(0, 0, width, height)
      
        // 绘制背景
        ctx.fillStyle = "#F9F9F9"
        ctx.fillRect(0, 0, width, height)
      
        // 根据图表类型绘制
        if (chartType === "pie") {
            drawPieChart(ctx, width, height)
        } else if (chartType === "bar") {
            drawBarChart(ctx, width, height)
        } else if (chartType === "line") {
            drawLineChart(ctx, width, height)
        }
        // ... 其他图表类型
    }
}
2.2 折线图实现
function drawLineChart(ctx, width, height) {
    // 绘制网格线
    ctx.strokeStyle = "#E0E0E0"
    ctx.lineWidth = 1
    for (var i = 0; i <= 5; i++) {
        var y = height * i / 5
        ctx.beginPath()
        ctx.moveTo(0, y)
        ctx.lineTo(width, y)
        ctx.stroke()
    }
  
    if (!chartData || chartData.length === 0) return
  
    // 计算绘图区域(留出坐标轴空间)
    var padding = 20
    var plotWidth = width - padding * 2
    var plotHeight = height - padding * 2
  
    // 计算所有点的坐标
    var points = []
    for (var i = 0; i < chartData.length; i++) {
        var point = chartData[i]
        points.push({
            x: padding + (point.x / 100) * plotWidth,
            y: padding + plotHeight - (point.y / 100) * plotHeight
        })
    }
  
    // 绘制折线
    ctx.strokeStyle = "#2196F3"
    ctx.lineWidth = 2
    ctx.beginPath()
    ctx.moveTo(points[0].x, points[0].y)
    for (var i = 1; i < points.length; i++) {
        ctx.lineTo(points[i].x, points[i].y)
    }
    ctx.stroke()
  
    // 绘制数据点
    ctx.fillStyle = "#2196F3"
    for (var i = 0; i < points.length; i++) {
        ctx.beginPath()
        ctx.arc(points[i].x, points[i].y, 4, 0, 2 * Math.PI)
        ctx.fill()
    }
  
    // 绘制坐标轴
    drawAxes(ctx, width, height, padding, plotWidth, plotHeight, "line")
}

技术要点

  • 使用 Canvas 2D Context API 进行绘制
  • 坐标转换:将数据坐标转换为屏幕坐标
  • 网格线辅助阅读
  • 数据点标记增强可视化
2.3 饼图实现
function drawPieChart(ctx, width, height) {
    var centerX = width / 2
    var centerY = height / 2
    var radius = Math.min(width, height) / 2 - 30
  
    if (!chartData || chartData.length === 0) return
  
    // 计算总和
    var total = 0
    for (var i = 0; i < chartData.length; i++) {
        total += chartData[i]
    }
    if (total === 0) return
  
    // 颜色数组
    var colors = ["#2196F3", "#4CAF50", "#FF9800", "#F44336", "#9C27B0"]
    var startAngle = -Math.PI / 2  // 从顶部开始
  
    for (var i = 0; i < chartData.length; i++) {
        var value = chartData[i]
        var sliceAngle = (value / total) * 2 * Math.PI
        var midAngle = startAngle + sliceAngle / 2
      
        // 第一个slice突出显示(exploded pie)
        var offsetRadius = (i === 0) ? radius + 10 : radius
        var offsetX = (i === 0) ? Math.cos(midAngle) * 10 : 0
        var offsetY = (i === 0) ? Math.sin(midAngle) * 10 : 0
      
        // 绘制扇形
        ctx.fillStyle = colors[i % colors.length]
        ctx.beginPath()
        ctx.moveTo(centerX + offsetX, centerY + offsetY)
        ctx.arc(centerX + offsetX, centerY + offsetY, offsetRadius, 
                startAngle, startAngle + sliceAngle)
        ctx.closePath()
        ctx.fill()
      
        // 绘制标签
        if (i === 0) {
            var labelX = centerX + offsetX + Math.cos(midAngle) * (offsetRadius + 15)
            var labelY = centerY + offsetY + Math.sin(midAngle) * (offsetRadius + 15)
            ctx.fillStyle = "#333333"
            ctx.font = "12px sans-serif"
            ctx.textAlign = "center"
            ctx.textBaseline = "middle"
            ctx.fillText("P" + i, labelX, labelY)
        }
      
        startAngle += sliceAngle
    }
}

设计亮点

  • 支持扇形分离效果(exploded pie)
  • 自动颜色分配
  • 标签显示
2.4 面积图实现(平滑曲线)

面积图使用 Catmull-Rom 样条插值实现平滑曲线效果:

function drawAreaChart(ctx, width, height) {
    // ... 网格线和数据点计算(同折线图)
  
    // 绘制填充区域
    ctx.fillStyle = "rgba(33, 150, 243, 0.3)"
    ctx.beginPath()
    var bottomY = padding + plotHeight
    ctx.moveTo(points[0].x, bottomY)
  
    // Catmull-Rom样条插值实现平滑曲线
    for (var i = 0; i < points.length - 1; i++) {
        var p0 = i > 0 ? points[i - 1] : points[i]
        var p1 = points[i]
        var p2 = points[i + 1]
        var p3 = i < points.length - 2 ? points[i + 2] : points[i + 1]
      
        // Catmull-Rom样条插值
        for (var t = 0; t <= 1; t += 0.1) {
            var t2 = t * t
            var t3 = t2 * t
          
            var x = 0.5 * ((2 * p1.x) +
                           (-p0.x + p2.x) * t +
                           (2 * p0.x - 5 * p1.x + 4 * p2.x - p3.x) * t2 +
                           (-p0.x + 3 * p1.x - 3 * p2.x + p3.x) * t3)
          
            var y = 0.5 * ((2 * p1.y) +
                           (-p0.y + p2.y) * t +
                           (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t2 +
                           (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t3)
          
            ctx.lineTo(x, y)
        }
    }
  
    ctx.lineTo(points[points.length - 1].x, bottomY)
    ctx.closePath()
    ctx.fill()
  
    // 绘制平滑曲线边界
    // ... 使用相同的插值算法绘制曲线
}

算法要点

  • Catmull-Rom 样条插值实现平滑曲线
  • 填充区域从曲线到X轴底部
  • 半透明填充增强视觉效果
2.5 雷达图实现
function drawRadarChart(ctx, width, height) {
    var centerX = width / 2
    var centerY = height / 2
    var radius = Math.min(width, height) / 2 - 30
  
    // 绘制网格圆
    ctx.strokeStyle = "#E0E0E0"
    ctx.lineWidth = 1
    for (var r = 0.2; r <= 1; r += 0.2) {
        ctx.beginPath()
        ctx.arc(centerX, centerY, radius * r, 0, 2 * Math.PI)
        ctx.stroke()
    }
  
    // 绘制数据区域
    ctx.fillStyle = "rgba(33, 150, 243, 0.3)"
    ctx.strokeStyle = "#2196F3"
    ctx.lineWidth = 2
    ctx.beginPath()
  
    for (var i = 0; i < chartData.length; i++) {
        var point = chartData[i]
        // X轴是角度(0, 60, 120, 180, 240, 300度)
        var angle = (point.x * Math.PI / 180) - Math.PI / 2
        var r = (point.y / 100) * radius
        var x = centerX + Math.cos(angle) * r
        var y = centerY + Math.sin(angle) * r
      
        if (i === 0) {
            ctx.moveTo(x, y)
        } else {
            ctx.lineTo(x, y)
        }
    }
  
    ctx.closePath()
    ctx.fill()
    ctx.stroke()
}

特点

  • 极坐标系统
  • 圆形网格辅助线
  • 多边形数据区域填充
2.6 热力图实现
function drawHeatMapChart(ctx, width, height) {
    if (!chartData || chartData.length === 0) return
  
    var cols = 5
    var rows = Math.ceil(chartData.length / cols)
    var cellWidth = (width - 20) / cols
    var cellHeight = (height - 20) / rows
  
    // 计算最大值
    var maxValue = 0
    for (var j = 0; j < chartData.length; j++) {
        if (chartData[j] > maxValue) {
            maxValue = chartData[j]
        }
    }
    if (maxValue === 0) maxValue = 1
  
    // 绘制热力单元格
    for (var i = 0; i < chartData.length; i++) {
        var value = chartData[i]
        var row = Math.floor(i / cols)
        var col = i % cols
        var x = 10 + col * cellWidth
        var y = 10 + row * cellHeight
      
        // 根据值计算颜色强度(蓝色渐变)
        var intensity = value / maxValue
        var r = Math.floor(33 + intensity * 222)   // 33-255
        var g = Math.floor(150 - intensity * 100)   // 150-50
        var b = Math.floor(243 - intensity * 100)   // 243-143
      
        ctx.fillStyle = "rgb(" + r + "," + g + "," + b + ")"
        ctx.fillRect(x + 2, y + 2, cellWidth - 4, cellHeight - 4)
    }
}

实现要点

  • 网格布局计算
  • 颜色强度映射
  • 蓝色渐变配色方案

3. 动态图表实现

动态图表支持实时数据更新,使用 Timer 组件定期添加新数据点:

Canvas {
    property var dataPoints: []  // 动态数据点数组
    property int maxPoints: 30    // 最大点数
  
    function drawDynamicChart(ctx, width, height) {
        // ... 绘制逻辑(类似折线图)
      
        // 动态调整X轴位置
        var dx = 100 / 10
        var less = 10 - dataPoints.length
      
        // 计算点坐标
        var points = []
        for (var i = 0; i < dataPoints.length; i++) {
            points.push({
                x: padding + ((less * dx + i * dx) / 100) * plotWidth,
                y: padding + plotHeight - (dataPoints[i] / 100) * plotHeight
            })
        }
      
        // 绘制平滑曲线
        // ... 使用Catmull-Rom插值
    }
  
    Timer {
        interval: 1000  // 每秒更新
        running: chartType === "dynamic"
        repeat: true
        onTriggered: {
            // 添加新数据点
            var newValue = Math.random() * 100
            dataPoints.push(newValue)
          
            // 限制最大点数(滑动窗口)
            if (dataPoints.length > maxPoints) {
                dataPoints.shift()
            }
          
            // 请求重绘
            requestPaint()
        }
    }
  
    Component.onCompleted: {
        if (chartType === "dynamic") {
            // 初始化数据
            for (var i = 0; i < 5; i++) {
                dataPoints.push(Math.random() * 100)
            }
        }
        requestPaint()
    }
}

技术要点

  • Timer 组件实现定时更新
  • 滑动窗口机制限制数据点数量
  • requestPaint() 触发Canvas重绘
  • 平滑曲线算法保证视觉效果

4. 数据生成算法

项目使用基于种子的伪随机数生成器,确保每个图表的数据稳定且可重现:

function generateChartData(type, seed) {
    // 计算种子值
    var seedValue = 12345
    if (seed && seed.length > 0) {
        for (var i = 0; i < seed.length; i++) {
            var code = seed.charCodeAt(i)
            if (isFinite(code)) {
                seedValue += code
            }
        }
    }
  
    // 线性同余生成器(LCG)
    var data = []
    var chartTypeLocal = getChartType(type)
  
    if (chartTypeLocal === "pie") {
        // 饼图:5个数据点
        for (var i = 0; i < 5; i++) {
            seedValue = (seedValue * 9301 + 49297) % 233280
            data.push((seedValue / 233280) * 100)
        }
    } else if (chartTypeLocal === "scatter") {
        // 散点图:20个数据点
        for (var i = 0; i < 20; i++) {
            seedValue = (seedValue * 9301 + 49297) % 233280
            var x = (seedValue / 233280) * 100
            seedValue = (seedValue * 9301 + 49297) % 233280
            var y = (seedValue / 233280) * 100
            data.push({x: x, y: y})
        }
    }
    // ... 其他图表类型
  
    return data
}

算法特点

  • 基于字符串种子的哈希值
  • 线性同余生成器(LCG)保证随机性
  • 不同图表类型生成不同格式的数据

🔧 配置与构建

1. CMakeLists.txt 配置

cmake_minimum_required(VERSION 3.5.0)
project(QtForHOSample)

set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTORCC ON)

list(APPEND CMAKE_FIND_ROOT_PATH ${QT_PREFIX})

find_package(QT NAMES Qt5 Qt6 REQUIRED COMPONENTS Core Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS 
    Concurrent Gui Network Qml Quick QuickControls2 
    Widgets QuickTemplates2 QmlWorkerScript Charts)

add_library(entry SHARED main.cpp qml.qrc)

target_link_libraries(entry PRIVATE 
    Qt${QT_VERSION_MAJOR}::Concurrent
    Qt${QT_VERSION_MAJOR}::Core
    Qt${QT_VERSION_MAJOR}::Gui
    Qt${QT_VERSION_MAJOR}::Network
    Qt${QT_VERSION_MAJOR}::Qml
    Qt${QT_VERSION_MAJOR}::Quick
    Qt${QT_VERSION_MAJOR}::Widgets
    Qt${QT_VERSION_MAJOR}::QuickControls2
    Qt${QT_VERSION_MAJOR}::QuickTemplates2
    Qt${QT_VERSION_MAJOR}::QmlWorkerScript
    Qt${QT_VERSION_MAJOR}::Charts
    Qt${QT_VERSION_MAJOR}::QOpenHarmonyPlatformIntegrationPlugin
)

2. 资源文件配置

qml.qrc:

<?xml version="1.0" encoding="UTF-8"?>
<RCC>
    <qresource prefix="/">
        <file>main.qml</file>
        <file>DynamicChart.qml</file>
        <file>DynamicChartX.qml</file>
        <file>AreaChart.qml</file>
        <file>DateTimeChart.qml</file>
        <file>LineChart.qml</file>
        <file>StackedBarChart.qml</file>
        <file>PieChart.qml</file>
        <file>BarChart.qml</file>
        <file>ScatterChart.qml</file>
        <file>RadarChart.qml</file>
        <file>HeatMapChart.qml</file>
        <file>GaugeChart.qml</file>
    </qresource>
</RCC>

3. main.cpp 入口配置

#include <QQmlApplicationEngine>
#include <QQmlContext>
#include <QSurfaceFormat>
#include <QtQml>
#include <QDebug>
#include <QGuiApplication>
#include <QCoreApplication>
#include <QByteArray>
#include <QScreen>

static QQmlApplicationEngine *g_engine = nullptr;

extern "C" int qtmain(int argc, char **argv)
{
    if (g_engine != nullptr) {
        qDebug() << "Chart Qt引擎已初始化,跳过重复初始化";
        return 0;
    }
  
    // 启用高DPI缩放
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);
    QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps, true);
  
    // 设置 OpenGL ES
    QCoreApplication::setAttribute(Qt::AA_UseOpenGLES);
    QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
  
    QGuiApplication app(argc, argv);
    QCoreApplication::setApplicationName(QStringLiteral("Chart"));
  
    // 配置 OpenGL ES 表面格式
    QSurfaceFormat format;
    format.setAlphaBufferSize(8);
    format.setRenderableType(QSurfaceFormat::OpenGLES);
    format.setVersion(2, 0);
    format.setSwapBehavior(QSurfaceFormat::DoubleBuffer);
    QSurfaceFormat::setDefaultFormat(format);
  
    // 创建 QML 引擎
    g_engine = new QQmlApplicationEngine();
    g_engine->addImportPath(QStringLiteral("qrc:/"));
    g_engine->addImportPath(QStringLiteral("qrc:/qml"));
  
    // 加载 QML 文件
    const QUrl url(QStringLiteral("qrc:/main.qml"));
    g_engine->load(url);
  
    return app.exec();
}

🐛 问题与解决方案

问题1: Canvas绘制性能问题

现象: 图表数量多时,界面卡顿

原因分析:

  • Canvas重绘频率过高
  • 没有使用缓存机制
  • 复杂的计算在绘制函数中重复执行

解决方案:

Canvas {
    // 启用Canvas缓存
    renderTarget: Canvas.FramebufferObject
  
    // 数据变化时才重绘
    onChartDataChanged: requestPaint()
  
    // 避免在onPaint中进行复杂计算
    property var cachedPoints: []  // 缓存计算好的点坐标
  
    function updatePoints() {
        // 预先计算点坐标
        cachedPoints = []
        for (var i = 0; i < chartData.length; i++) {
            // ... 计算逻辑
            cachedPoints.push({x: x, y: y})
        }
    }
  
    onPaint: {
        // 直接使用缓存的点坐标
        for (var i = 0; i < cachedPoints.length; i++) {
            // ... 绘制
        }
    }
}

问题2: 高DPI显示模糊

现象: 在高DPI屏幕上图表显示模糊

原因分析:

  • Canvas没有考虑设备像素比
  • 字体大小没有缩放

解决方案:

ApplicationWindow {
    // 启用高DPI缩放
    // 在main.cpp中已设置
    // QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling, true);
  
    // 使用scaleFactor统一缩放
    readonly property real scaleFactor: width > 1500 ? 2.0 : 1.5
  
    Canvas {
        // Canvas会自动处理高DPI
        // 但需要确保字体大小也缩放
        onPaint: {
            ctx.font = (10 * scaleFactor) + "px sans-serif"
        }
    }
}

问题3: 动态图表内存泄漏

现象: 长时间运行后内存占用持续增长

原因分析:

  • dataPoints 数组无限增长
  • Timer没有正确停止

解决方案:

Timer {
    interval: 1000
    running: chartType === "dynamic" && visible  // 添加visible检查
    repeat: true
    onTriggered: {
        var newValue = Math.random() * 100
        dataPoints.push(newValue)
      
        // 限制最大点数(滑动窗口)
        if (dataPoints.length > maxPoints) {
            dataPoints.shift()  // 移除最旧的数据
        }
      
        requestPaint()
    }
}

Component.onDestruction: {
    // 清理数据
    dataPoints = []
}

问题4: 坐标轴标签重叠

现象: 数据点密集时,坐标轴标签重叠

原因分析:

  • 固定间隔绘制标签
  • 没有根据数据密度动态调整

解决方案:

function drawAxes(ctx, width, height, padding, plotWidth, plotHeight, chartType) {
    // 动态计算标签数量
    var labelCount = Math.min(5, Math.max(2, Math.floor(plotWidth / 80)))
  
    ctx.fillStyle = "#666666"
    ctx.font = "10px sans-serif"
    ctx.textAlign = "center"
  
    // X轴标签
    for (var i = 0; i <= labelCount; i++) {
        var x = padding + (plotWidth * i / labelCount)
        var label = Math.round(i * 100 / labelCount)
        ctx.fillText(label.toString(), x, padding + plotHeight + 5)
    }
  
    // Y轴标签类似处理
}

🎯 优化技巧

1. 性能优化

  • Canvas缓存: 使用 renderTarget: Canvas.FramebufferObject 启用硬件加速
  • 数据预处理: 在数据变化时预先计算坐标,避免在 onPaint 中重复计算
  • 按需重绘: 只在数据变化或窗口大小变化时调用 requestPaint()
  • 虚拟化: 对于大量图表,考虑使用 ListView 的虚拟化机制

2. 代码优化

  • 函数复用: 提取公共绘制函数(如 drawAxes
  • 类型检查: 添加数据有效性检查,避免运行时错误
  • 常量提取: 将颜色、尺寸等常量提取为属性
readonly property color chartColor: "#2196F3"
readonly property color gridColor: "#E0E0E0"
readonly property int gridLineCount: 5

3. 用户体验优化

  • 加载动画: 数据加载时显示加载指示器
  • 交互反馈: 鼠标悬停时高亮数据点
  • 工具提示: 显示数据点的具体数值
  • 响应式设计: 适配不同屏幕尺寸

📊 效果展示

项目实现了11种图表类型,每种图表都有独特的特点:

  1. 折线图: 清晰的趋势展示,带网格线和数据点标记
  2. 柱状图: 直观的数值对比,支持多系列
  3. 饼图: 比例关系可视化,支持扇形分离效果
  4. 散点图: 相关性分析,双轴数据展示
  5. 面积图: 平滑曲线填充,适合趋势展示
  6. 雷达图: 多维度数据对比,极坐标系统
  7. 热力图: 数据密度可视化,颜色强度映射
  8. 堆叠柱状图: 多系列数据堆叠展示
  9. 日期时间图: 时间序列数据展示
  10. 动态曲线图: 实时数据更新,平滑动画
  11. 仪表盘图: 单值指标展示(如进度、百分比)

🎓 学习要点

通过这个项目,你将学到:

  1. Canvas 2D API: 掌握QML Canvas的绘制API和最佳实践
  2. 图表算法: 学习坐标转换、插值算法、数据可视化原理
  3. QML组件化: 如何设计可复用的图表组件
  4. 响应式设计: 适配不同屏幕尺寸和高DPI显示
  5. 性能优化: Canvas绘制性能优化技巧
  6. 数据绑定: QML数据绑定和状态管理
  7. 动画效果: 平滑曲线插值和动态更新

🚀 扩展方向

基于现有功能,你可以继续扩展:

  1. 交互功能:

    • 数据点点击事件
    • 缩放和平移
    • 图例点击切换显示
  2. 更多图表类型:

    • 3D图表
    • 甘特图
    • 树状图
    • 桑基图
  3. 数据源集成:

    • 连接真实数据API
    • 支持CSV/JSON数据导入
    • 数据库查询集成
  4. 导出功能:

    • 导出为图片(PNG/SVG)
    • 导出为PDF报告
    • 打印支持
  5. 主题系统:

    • 深色模式
    • 自定义配色方案
    • 主题切换动画
  6. 高级特性:

    • 数据筛选和排序
    • 多图表联动
    • 数据标注和注释

📚 参考资源

💡 总结

本文通过一个完整的图表组件库项目,展示了如何使用 Qt QML Canvas API 在 HarmonyOS 平台上实现自定义图表绘制。项目涵盖了11种常见图表类型,从基础的折线图、柱状图到复杂的雷达图、热力图,展示了Canvas绘制的强大能力。

项目的关键技术点包括:

  • Canvas 2D Context API 的使用
  • 坐标系统和数据转换
  • 平滑曲线插值算法
  • 动态数据更新机制
  • 响应式布局设计
  • 性能优化技巧

这个项目不仅是一个实用的图表组件库,更是学习 Qt QML 图形编程和 HarmonyOS 开发的优秀案例。希望这个项目能帮助你快速上手 Qt + HarmonyOS 的图形应用开发!


请添加图片描述

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐