第24章 Qt Charts图表:折线图、柱状图、饼图、散点图、图表交互、动态数据更新

图表开发,说白了就是把枯燥的数字变成直观的图形。我做了这么多年Qt开发,几乎每个项目都离不开图表——监控系统要实时曲线,报表工具要柱状对比,数据分析要散点分布。Qt Charts模块帮了大忙,它原生集成在Qt框架里,不需要额外装第三方库。

这一章,我会带你走一遍Qt Charts的核心用法。折线图、柱状图、饼图、散点图,每种图我都会讲清楚怎么搭架子、怎么填数据。最后再聊聊图表交互和动态更新——嗯,这部分在实际项目中特别重要。

24.1 Qt Charts模块概览

Qt Charts是Qt官方提供的2D图表组件。它基于Graphics View框架,所以渲染性能不错。支持的图表类型包括:

  • QLineSeries — 折线图/曲线图
  • QBarSeries — 柱状图
  • QPieSeries — 饼图
  • QScatterSeries — 散点图
  • QAreaSeries — 面积图
  • QPolarChart — 极坐标图

使用前记得在.pro文件里加上:

QT += charts

头文件引用:

#include <QtCharts>
using namespace QtCharts;
小提示: 如果你用的是Qt 5.7以上版本,Qt Charts已经内置在安装包里了。老版本可能需要单独编译。我个人习惯在项目一开始就把charts模块加上,免得后面改.pro文件忘了重新qmake。

24.2 折线图:最常用的趋势展示

折线图适合展示数据随时间的变化趋势。比如CPU使用率、温度曲线、股票走势。我当年做工业监控项目时,折线图几乎占了半个界面。

先看一个最简单的例子:

// 创建图表
QChart *chart = new QChart();
chart->setTitle("CPU温度监控");

// 创建折线序列
QLineSeries *series = new QLineSeries();
series->setName("CPU温度");

// 添加数据点
series->append(0, 36.5);
series->append(1, 38.2);
series->append(2, 42.1);
series->append(3, 45.8);
series->append(4, 44.3);

// 把序列加到图表
chart->addSeries(series);

// 创建坐标轴
QValueAxis *axisX = new QValueAxis();
axisX->setTitleText("时间(秒)");
axisX->setRange(0, 5);
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);

QValueAxis *axisY = new QValueAxis();
axisY->setTitleText("温度(℃)");
axisY->setRange(30, 50);
chart->addAxis(axisY, Qt::AlignLeft);
series->attachAxis(axisY);

// 显示图表
QChartView *chartView = new QChartView(chart);
chartView->setRenderHint(QPainter::Antialiasing);
setCentralWidget(chartView);

这段代码跑起来,你会看到一条从(0,36.5)到(4,44.3)的折线。注意坐标轴要先addAxis再attachAxis,顺序错了图表会不显示。

关键点: 折线图的数据点用append()添加,x和y都是double类型。如果数据量很大(比如上万点),建议用QLineSeries的replace()方法批量替换,性能会好很多。

24.3 柱状图:对比数据的好帮手

柱状图适合做分类对比。比如各月销售额、各产品销量。柱状图比折线图稍微复杂一点,因为它有分组的概念。

// 创建柱状图序列
QBarSet *set1 = new QBarSet("一季度");
QBarSet *set2 = new QBarSet("二季度");

*set1 << 120 << 150 << 180;
*set2 << 200 << 170 << 190;

QBarSeries *series = new QBarSeries();
series->append(set1);
series->append(set2);

// 创建图表
QChart *chart = new QChart();
chart->addSeries(series);
chart->setTitle("季度销售额对比");

// 分类轴
QBarCategoryAxis *axisX = new QBarCategoryAxis();
axisX->append("一月");
axisX->append("二月");
axisX->append("三月");
chart->addAxis(axisX, Qt::AlignBottom);
series->attachAxis(axisX);

// 数值轴
QValueAxis *axisY = new QValueAxis();
axisY->setRange(0, 250);
chart->addAxis(axisY, Qt::AlignLeft);
series->attachAxis(axisY);

这里用了QBarCategoryAxis做分类轴,它会自动把"一月""二月""三月"显示在X轴底部。每个QBarSet代表一组柱子,颜色会自动分配。

注意: 柱状图的X轴是分类轴,不是数值轴。如果你想把柱状图做成直方图(连续数值),需要用QBarSeries配合QValueAxis,但那样需要自己处理柱子的宽度和间距。我曾经在这个坑里折腾了半天。

24.4 饼图:占比一目了然

饼图展示各部分占总体的比例。Qt的饼图用QPieSeries,用法很直观:

QPieSeries *series = new QPieSeries();
series->append("Android", 68.5);
series->append("iOS", 29.3);
series->append("其他", 2.2);

// 让某一块突出显示
QPieSlice *slice = series->slices().at(0);
slice->setExploded(true);
slice->setLabelVisible(true);

QChart *chart = new QChart();
chart->addSeries(series);
chart->setTitle("移动操作系统市场份额");

饼图的每个扇区叫QPieSlice。你可以单独设置它的颜色、标签、是否突出。setExploded(true)会让扇区往外弹出一段,视觉上更醒目。

我个人习惯在饼图上显示百分比,可以用setLabelVisible(true)配合setLabelFormat():

slice->setLabelVisible(true);
slice->setLabel(QString("%1: %2%")
    .arg(slice->label())
    .arg(slice->percentage() * 100, 0, 'f', 1));

24.5 散点图:发现数据分布规律

散点图用来观察两个变量之间的关系。比如身高和体重、广告投入和销售额。Qt用QScatterSeries实现:

QScatterSeries *series = new QScatterSeries();
series->setName("样本数据");
series->setMarkerSize(10);  // 点的大小

// 随机添加50个点
for (int i = 0; i < 50; ++i) {
    series->append(rand() % 100, rand() % 100);
}

QChart *chart = new QChart();
chart->addSeries(series);
chart->createDefaultAxes();  // 自动创建坐标轴
chart->setTitle("散点图示例");

createDefaultAxes()是个偷懒的好方法,它会根据数据自动创建合适的坐标轴。不过自动创建的轴不一定完美,我建议还是手动创建并设置范围。

散点图的标记形状可以改:

series->setMarkerShape(QScatterSeries::MarkerShapeCircle);   // 圆形
series->setMarkerShape(QScatterSeries::MarkerShapeRectangle); // 方形
series->setMarkerShape(QScatterSeries::MarkerShapeTriangle);  // 三角形

24.6 图表交互:让用户动起来

光显示图表还不够,用户需要交互。Qt Charts提供了几种内置交互方式:

  • 缩放 — 鼠标滚轮缩放,按住右键拖动
  • 平移 — 按住左键拖动
  • 悬停提示 — 鼠标悬停时显示数据值
  • 点击选中 — 点击某个数据点触发事件

启用交互很简单:

chartView->setRubberBand(QChartView::RectangleRubberBand);

这样用户就可以用鼠标框选区域进行缩放。如果想禁用缩放,可以设置:

chart->setZoomInFactor(1.0);  // 禁止缩放

悬停提示需要连接信号:

connect(series, &QLineSeries::hovered,
        [](const QPointF &point, bool state) {
    if (state) {
        qDebug() << "数据点:" << point.x() << point.y();
    }
});

点击事件类似:

connect(series, &QLineSeries::clicked,
        [](const QPointF &point) {
    qDebug() << "点击了:" << point;
});
经验之谈: 我在做金融数据看板时,用户要求鼠标悬停时显示一个tooltip。我用了QToolTip::showText()配合hovered信号,效果很好。注意在悬停结束时(state=false)要隐藏tooltip。

24.7 动态数据更新:让图表活起来

实际项目中,数据往往是实时更新的。比如传感器读数、股票价格。Qt Charts支持动态更新,核心思路是定时器+数据追加。

看一个实时折线图的例子:

// 头文件
QLineSeries *m_series;
QChart *m_chart;
int m_x = 0;

// 初始化
m_series = new QLineSeries();
m_chart->addSeries(m_series);

// 定时器更新
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, [this]() {
    double y = qrand() % 100;  // 模拟数据
    m_series->append(m_x++, y);

    // 限制数据点数量,防止内存爆炸
    if (m_series->count() > 100) {
        m_series->remove(0);
    }

    // 自动滚动X轴
    if (m_x > 100) {
        m_chart->axisX()->setRange(m_x - 100, m_x);
    }
});
timer->start(100);  // 每100毫秒更新一次

这里有几个关键点:

  • 用append()添加新数据点
  • 用remove(0)删除旧数据,保持固定窗口
  • 动态调整X轴范围,实现滚动效果

对于柱状图的动态更新,可以用QBarSet的replace()方法:

QBarSet *set = series->barSets().at(0);
set->replace(0, newValue1);
set->replace(1, newValue2);

饼图更新更简单,直接修改QPieSlice的value:

QPieSlice *slice = series->slices().at(0);
slice->setValue(newValue);
性能警告: 动态更新时,如果数据点超过几千个,界面会卡顿。我的经验是:折线图控制在5000点以内,散点图控制在2000点以内。如果数据量更大,考虑用QXYSeries的replace()批量替换,或者做数据降采样。

24.8 综合示例:多图表联动

最后,我展示一个多图表联动的场景。假设我们要做一个设备监控面板,同时显示温度折线图、湿度柱状图、报警饼图。

核心思路是用QSplitter分割布局,每个图表独立更新:

// 主布局
QSplitter *splitter = new QSplitter(Qt::Horizontal);

// 温度折线图
QChartView *tempView = createTempChart();
splitter->addWidget(tempView);

// 湿度柱状图
QChartView *humidView = createHumidChart();
splitter->addWidget(humidView);

// 报警饼图
QChartView *alarmView = createAlarmChart();
splitter->addWidget(alarmView);

setCentralWidget(splitter);

每个图表的更新函数独立,通过一个全局定时器统一触发:

QTimer *globalTimer = new QTimer(this);
connect(globalTimer, &QTimer::timeout, [this]() {
    updateTempChart();   // 更新温度
    updateHumidChart();  // 更新湿度
    updateAlarmChart();  // 更新报警
});
globalTimer->start(500);  // 0.5秒刷新一次

这样做的好处是各图表互不干扰,更新频率可以单独控制。我在实际项目中就是这么做的,用户反馈很流畅。

24.9 本章知识体系

下面这张图总结了Qt Charts的核心知识点和它们之间的关系:

Qt Charts 折线图 QLineSeries 柱状图 QBarSeries 饼图 QPieSeries 散点图 QScatterSeries 坐标轴:QValueAxis / QBarCategoryAxis 交互:缩放 / 平移 / 悬停 / 点击 动态更新:定时器 + append/remove/replace 性能优化:控制数据量 / 批量替换 / 降采样 综合应用:多图表联动 / QSplitter布局

从这张图可以看出,Qt Charts的核心是五种图表类型,它们共享坐标轴和交互机制。动态更新和性能优化是实际项目中的关键,最后通过综合应用把各部分串联起来。


好了,这一章的内容就到这里。Qt Charts其实还有很多高级功能,比如自定义图例、动画效果、主题切换。但我觉得先把基础打牢更重要。你可以在自己的项目里试试这些代码,改改数据看看效果。遇到问题别急,多看看Qt官方文档,里面有很多示例。

一句话总结: Qt Charts让数据可视化变得简单。折线图看趋势,柱状图做对比,饼图显占比,散点图找规律。加上交互和动态更新,你的应用就能「活」起来。

公众号:蓝海资料掘金营,微信deep3321