资讯动态

Qt5 QtChart二维图表开发实战:从工程配置到实时曲线封装方案

发布时间:2026/10/14 20:55:53 来源:尧图企业网站定制
在 Qt5 里做二维图表我最早是用 QPainter 自己画网格、画刻度的数据稍微多一点、需求稍微改一版我就想把之前的代码全推翻。后来切到 QtChart才意识到很多重复劳动官方早就帮你做好了。这篇想聊的是我基于 Qt5 QtChart 绘制二维图表的完整实践从工程配置、核心类拆解到折线图/柱状图/饼图/实时刷新曲线的可运行代码再到实际项目里踩过的坑以及一套轻量封装方案。适合刚上手 Qt 的初学者也适合正在用 QPainter 手绘图表的界面开发者。1. 为什么二维图表选 QtChart而不是 QPainter 手绘1.1 先聊聊市面上几个方案的区别很多人刚开始接触 Qt 绘图时第一反应是用 QPainter 画坐标轴、画网格线、再画数据曲线。这个方案不是不能做而是维护成本太高。坐标轴的刻度怎么算、标签怎么放、图例怎么画、鼠标悬停显示数值怎么做、缩放后刻度怎么重排这些问题全都要自己解决。我第一版监控界面就是这么写的后来要加曲线颜色、加图例、加双坐标轴光是坐标换算的代码就写了一百多行最后还是拆掉重写了。除了手绘常见选择还有 Qwt 和 QCustomPlot。Qwt 是老牌库功能确实全但风格偏老Qt 高版本适配也需要额外配置新人上手容易被编译问题劝退。QCustomPlot 轻量、性能好、文档实例多适合对图表有重度定制需求的项目但它是第三方库升级维护和许可证都是需要考虑的因素。QtChart 是官方模块和 QWidget、QSS、事件体系天然一致配置起来最省心。我用一张表总结这几种方案的差别。方案上手成本图表交互维护风险适合场景QPainter 手绘高基本没有完全自己维护画个简单示意图Qwt中高较强第三方更新慢老项目、专业工业图QCustomPlot中很强第三方依赖作者更新海量数据、重度定制QtChart低够用官方维护随 Qt 升级监控曲线、统计报表1.2 什么场景值得用它QtChart 最适合的场景是“二维图表”里那 90% 的常规需求实时监控曲线、性能走势、统计柱状图、占比饼图、散点分布。官方把坐标轴、图例、主题、动画、缩放这些底层能力都封装好了你只需要把数据填进去图表就能正常展示。它的边界也要认清QtChart 不是万能的。它本身没有 3D 图表能力复杂 K 线图需要自己用 QLineSeries 或者自定义绘制去拼海量数据点的渲染性能也不如专门做高性能绘图的第三方库。如果项目明确需要上千个点、频繁全量刷新建议先做原型测试再决定是不是要换方案。至少在我做过的上位机和数据看板里QtChart 完全够用。2. Qt5 工程里把 charts 模块挂进来的正确姿势2.1 qmake 和 CMake 两种写法Qt Charts 从 Qt 5.7 开始成为官方模块开源版也能直接用。用 qmake 的话只需要在.pro文件里加一行chartsQT core gui charts greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET ChartDemo TEMPLATE app SOURCES main.cpp HEADERS chartwidget.h注意这里的顺序不影响结果但charts一定要写不写的话编译时头文件能找到链接阶段却会报一堆 undefined reference。这个错我见过不少新手踩。如果是 CMake 工程需要显式查找 Charts 组件并链接。Qt5 里 Charts 的 CMake 包名是Qt5Charts所以最稳妥的写法是find_package(Qt5 REQUIRED COMPONENTS Core Gui Widgets) find_package(Qt5Charts REQUIRED) add_executable(ChartDemo main.cpp chartwidget.cpp) target_link_libraries(ChartDemo PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets Qt5::Charts )如果你用的在线安装器安装 Qt记得勾选 Charts 模块这个模块体积不大但是默认安装不一定勾选。Linux 下如果用发行版仓库安装 Qt也需要单独安装对应的 Qt Charts 开发包。2.2 跑通一个最小窗口先别急着往上堆业务先把一个能跑的最小窗口搭起来验证环境没问题。下面这段代码是我每次都用来测环境的模板#include QApplication #include QtCharts/QChartView #include QtCharts/QLineSeries #include QtCharts/QValueAxis using namespace QtCharts; int main(int argc, char *argv[]) { QApplication app(argc, argv); QLineSeries *series new QLineSeries(); series-append(0, 6); series-append(2, 4); series-append(3, 8); series-append(7, 4); series-append(10, 7); QChart *chart new QChart(); chart-addSeries(series); chart-legend()-hide(); chart-createDefaultAxes(); chart-setTitle(QStringLiteral(最简单的折线图)); chart-setAnimationOptions(QChart::SeriesAnimations); QChartView *chartView new QChartView(chart); chartView-setRenderHint(QPainter::Antialiasing); chartView-resize(800, 500); chartView-show(); return app.exec(); }这里有个关键点createDefaultAxes()必须放在addSeries(series)之后调用而且系列里要有数据。因为 QtChart 会根据已有数据范围来创建坐标轴如果数据是空的轴就不会有合理的范围。这个顺序写反了你会在界面上看到一条“看不见”的曲线。setRenderHint(QPainter::Antialiasing)也很重要主要为了曲线边缘平滑。QChartView 本身继承自 QGraphicsView所以它可以直接作为窗口显示也可以放在任何布局里。动画我一般只在原型阶段开正式跑大量数据的时候会关掉。3. 拆解核心类QChart/QChartView/轴/系列搞懂再写代码3.1 QChart 和 QChartView 的分工很多新手刚接触 QtChart 时会搞不清 QChart 和 QChartView 的区别经常把两个类混着用。打个比方QChart 更像是画布本身它负责管理系列、坐标轴、图例、主题这些图表内容QChartView 则是一块嵌在窗口里的画框负责把画布显示出来同时接收鼠标键盘事件。把图表放进界面的标准方式是QChartView *chartView new QChartView(chart); ui-layout-addWidget(chartView);QChart 本身是 QGraphicsWidget不是普通 QWidget所以一般不会直接把 QChart 塞进 QWidget 布局里而是通过 QChartView 来承载。如果你需要把图表嵌入一个已有场景中也可以把 QChart 添加到 QGraphicsScene 里但大多数场景直接用 QChartView 就够了。3.2 坐标轴不要总是用默认的createDefaultAxes()虽然方便但很多时候满足不了需求。至少在我做的项目里最终都会换成手动创建坐标轴因为默认轴的范围、刻度数量、标签格式经常不符合业务要求。QtChart 提供三种常用轴QValueAxis数值轴适合连续数值。QBarCategoryAxis分类轴适合柱状图、离散分类。QDateTimeAxis时间轴适合按时间显示的数据。手动添加轴的写法是QValueAxis *axisX new QValueAxis(); axisX-setRange(0, 100); axisX-setTitleText(QStringLiteral(时间)); axisX-setLabelFormat(%d); QValueAxis *axisY new QValueAxis(); axisY-setRange(0, 100); axisY-setTitleText(QStringLiteral(数值)); chart-addAxis(axisX, Qt::AlignBottom); chart-addAxis(axisY, Qt::AlignLeft); series-attachAxis(axisX); series-attachAxis(axisY);这里要强调的是addAxis只是把轴放到图表的指定位置它并不会自动关联到某个系列。必须再调series-attachAxis(axisX)轴和系列才会建立关系。很多初学者只加了轴忘记 attach结果图表上显示不出数据。多系列共用同一个 Y 轴时每个系列都要调一次 attachAxis。3.3 图例、主题、动画这些“看起来小”的配置图例默认是显示在图表右侧的。有的场景不需要图例可以chart-legend()-hide()。需要显示的话可以调整位置chart-legend()-setVisible(true); chart-legend()-setAlignment(Qt::AlignBottom); chart-legend()-setLabelColor(Qt::darkGray);主题很实用。QtChart 内置了几个主题切深色主题做夜间看板很方便chart-setTheme(QChart::ChartThemeDark);动画方面QChart::SeriesAnimations会在数据变化时做过渡动画看起来效果不错但数据刷新频率高的时候会拖累性能。实时曲线建议关掉或者只在初始加载时开一次。4. 四种常用二维图表的落地代码折线、柱状、饼图、实时曲线4.1 折线图多个系列共用或独立坐标轴折线图是 QtChart 里最常用的图也是我先讲的原因。单系列折线很简单多系列折线只需要创建多个QLineSeries统一加到 chart 上。多组数据数值范围不同的时候需要拆分左右两个 Y 轴。示例场景是监控页同时展示“CPU 温度”和“内存占用”温度在 0 到 100内存占用也在 0 到 100但它们不是同一个量纲仪表盘习惯上分左右轴显示QLIneSeries *tempSeries new QLineSeries(); tempSeries-setName(QStringLiteral(温度)); QLIneSeries *memSeries new QLineSeries(); memSeries-setName(QStringLiteral(内存)); for (int i 0; i 50; i) { tempSeries-append(i, 45 20 * qSin(i / 5.0)); memSeries-append(i, 30 15 * qCos(i / 6.0)); } chart-addSeries(tempSeries); chart-addSeries(memSeries); QValueAxis *axisX new QValueAxis(); axisX-setRange(0, 50); chart-addAxis(axisX, Qt::AlignBottom); tempSeries-attachAxis(axisX); memSeries-attachAxis(axisX); QValueAxis *axisY new QValueAxis(); axisY-setRange(0, 100); chart-addAxis(axisY, Qt::AlignLeft); tempSeries-attachAxis(axisY); QValueAxis *axisYRight new QValueAxis(); axisYRight-setRange(0, 100); chart-addAxis(axisYRight, Qt::AlignRight); memSeries-attachAxis(axisYRight);双轴的核心逻辑很简单每个系列需要知道自己挂在哪个轴上轴可以复用也可以各用各的。这里最容易犯的错是忘记把 axisX 也 attach 给 memSeries结果只有温度曲线有 X 轴内存曲线画不出来。4.2 柱状图统计类需求别用折线硬凹柱状图是统计报表里另一类高频需求。QtChart 用QBarSeriesQBarSet实现QBarSet代表一组柱子。QBarSet *setA new QBarSet(QStringLiteral(一季度)); QBarSet *setB new QBarSet(QStringLiteral(二季度)); *setA 12 18 15 22; *setB 8 15 20 17; QBarSeries *barSeries new QBarSeries(); barSeries-append(setA); barSeries-append(setB); QChart *chart new QChart(); chart-addSeries(barSeries); chart-setTitle(QStringLiteral(销售统计)); QBarCategoryAxis *axisX new QBarCategoryAxis(); axisX-append(QStringList() QStringLiteral(一月) QStringLiteral(二月) QStringLiteral(三月) QStringLiteral(四月)); chart-addAxis(axisX, Qt::AlignBottom); barSeries-attachAxis(axisX); QValueAxis *axisY new QValueAxis(); axisY-setRange(0, 30); chart-addAxis(axisY, Qt::AlignLeft); barSeries-attachAxis(axisY);柱状图的 X 轴基本都配QBarCategoryAxisY 轴用QValueAxis。这里要注意添加分类轴时给的是字符串列表顺序就是柱子在图表上的顺序。setRange(0, 30)的 30 要大于数据最大值 22不然最上面一根柱子会顶到边界。柱子宽度可以用barSeries-setBarWidth(0.5)调整默认 0.5 左右看起来比较舒服。4.3 饼图少分类占比不建议超过 6 块饼图用QPieSeries它不需要坐标轴直接加序列就能显示。QPieSeries *pieSeries new QPieSeries(); pieSeries-append(QStringLiteral(已完成), 80); pieSeries-append(QStringLiteral(进行中), 15); pieSeries-append(QStringLiteral(未开始), 5); // 把第一块分离出来 QPieSlice *slice pieSeries-slices().at(0); slice-setLabelVisible(true); slice-setExploded(true); slice-setBrush(QColor(#5470c6)); QChart *chart new QChart(); chart-addSeries(pieSeries); chart-setTitle(QStringLiteral(任务进度));我一般会把第一块设置为分离状态这样饼图重点更突出。要让标签显示百分比可以设置slice-setLabel(QStringLiteral(已完成 80%));或者依赖默认标签。饼图适合分类少、占比清晰的场景分类超过六七个以后会变得很挤不如换柱状图。4.4 实时刷新曲线监控界面的性能要点监控界面最常见的需求是曲线每隔一段时间更新一个点。很多人的第一版代码是这样写的每次拿series-append()追加一个新点然后就完了。数据少的时候没问题但跑上一小时series 里会积累几万个点界面越来越卡。我的建议是固定一个显示窗口大小超过上限后整批替换数据不要逐点 remove。下面是一个用QTimerQLineSeries::replace的滚动曲线写法QLineSeries *series new QLineSeries(); QVectorQPointF buffer; const int maxPoints 300; QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, []() { static double x 0.0; x 0.1; double y qSin(x) 0.2 * qrand() / double(RAND_MAX); buffer.append(QPointF(x, y)); if (buffer.size() maxPoints) { buffer.remove(0, buffer.size() - maxPoints); } series-replace(buffer); }); timer-start(100);replace(buffer)是用一整份新的点集替换旧的比反复append remove的性能好很多。这里有个更进一步的门道当 X 轴范围固定时可以用axisX-setRange(x - maxRange, x)让曲线始终停留在可视窗口内而不是随着 X 值一直往右拉。监控看板几乎都会用到这个小技巧。4.5 散点图和面积图顺手就一起做了散点图用QScatterSeries和折线图的用法几乎一样区别是它不连线可以控制点的大小和形状。QScatterSeries *scatter new QScatterSeries(); scatter-setName(QStringLiteral(样本)); scatter-setMarkerSize(10.0); scatter-setBorderColor(QColor(#ee6666)); scatter-setBrush(QColor(238, 102, 102, 80)); for (int i 0; i 30; i) { scatter-append(rand() % 100, rand() % 100); } chart-addSeries(scatter); chart-createDefaultAxes();面积图需要两条边界线一条上边界、一条下边界然后合成QAreaSeriesQLineSeries *upper new QLineSeries(); QLineSeries *lower new QLineSeries(); // 填充上下两条线的数据... QAreaSeries *area new QAreaSeries(upper, lower); area-setName(QStringLiteral(面积)); area-setBrush(QColor(84, 112, 198, 60)); chart-addSeries(area);面积图需要注意透明度颜色最好半透明不然下层数据全被遮完了。5. 交互与视觉细节坐标显示、缩放框选、导出图片5.1 鼠标悬停和点击显示数值二维图表只有“画出来”是不够的用户需要知道某个数据点具体是多少。QLineSeries和QScatterSeries都继承了QXYSeries它提供了hovered和clicked信号。我常用 hovered 信号做一个状态栏坐标显示connect(series, QXYSeries::hovered, this, [this](const QPointF point, bool state) { if (state) { ui-statusLabel-setText( QStringLiteral(x %1, y %2) .arg(point.x()) .arg(point.y())); } });如果想要点击某个点弹出详情用clicked信号connect(series, QXYSeries::clicked, this, [this](const QPointF point) { QMessageBox::information( this, QStringLiteral(数据点), QStringLiteral(x %1\ny %2) .arg(point.x()) .arg(point.y())); });如果需要在图表空白区域显示十字线或者 tooltip仅仅靠 series 的信号不够需要响应 QChartView 的鼠标事件然后通过chart-mapToValue()把视图坐标换算成图表数据坐标。这个属于进阶定制二维图表的常规项目里不一定会用到但知道有这层映射关系后面遇到不会懵。5.2 框选放大和滚轮缩放QChartView 本身带了橡皮筋框选放大能力只要设置一下chartView-setRubberBand(QChartView::RectangleRubberBand);这样鼠标左键拖一个矩形框图表就会放大到框选区域。想要恢复原始大小调用chart-zoomReset();如果希望鼠标滚轮直接缩放需要继承 QChartView 重写wheelEvent。我试过最简单的方法void MyChartView::wheelEvent(QWheelEvent *event) { if (event-angleDelta().y() 0) { m_chart-zoomIn(); } else { m_chart-zoomOut(); } event-accept(); }这里有个踩坑点滚轮缩放默认围绕鼠标当前位置进行但 QtChart 的 zoomIn 是按比例放到中心点。想让缩放中心跟随鼠标要用chart-zoomIn()配合mapToValue换算逻辑上稍微绕一点。一般情况下默认缩放中心也够用。5.3 导出图片一个 grab 就搞定把图表导出成图片QtChart 的体验确实好。既然 QChartView 是 QWidget 体系里的控件直接这样拿图像QPixmap pixmap chartView-grab(); pixmap.save(QStringLiteral(chart.png));如果要导出透明背景的图片需要在保存前让图表背景透明chart-setBackgroundVisible(false);如果图表带着白色默认背景导出的 PNG 就会有个白底。做汇报截图、做报表导出时透明背景往往更讨喜。6. 数据量大就卡、中文乱码我把踩过的坑整理成排查表6.1 数据量一大就卡顿先砍这三个开关实时曲线的卡顿排查我一般按优先级砍三轮。第一关动画。设置QChart::NoAnimation动画在实时刷新场景里基本没有意义。第二关抗锯齿。chartView-setRenderHint(QPainter::Antialiasing, false)能提升一点绘制速度代价是曲线边缘有锯齿。如果机器性能一般先用这个换流畅度。第三控制数据点数量。任何图表都不可能无限制显示几百万个点。客户端数据显示讲究“聚焦有效信息”超过窗口范围的点就该被删除或者做抽稀。上节用replace控制maxPoints就是这个目的。如果数据量非常大QtChart 还提供了 OpenGL 加速以QLineSeries为例可以开启series-setUseOpenGL(true)不过它会限制一些交互和绘制能力。真正跑到几十万点、需要频繁刷新的场景我建议直接评估 QCustomPlot。现象可能原因处理方案每秒刷新后越来越卡series 点数无限增长固定点数用 replace 截断缩放时卡顿明显动画仍然开启关闭动画曲线锯齿明显但不想开抗锯齿绘制量太大减少数据点或开启 OpenGL多个系列同时刷新界面卡死每个系列各刷各的合并数据更新减少 paint 次数6.2 坐标轴刻度“乱跳”或显示不下坐标轴刻度问题几乎是图表需求里最常被提的。默认轴的刻度数量是自动算的碰到小数或者特别大的数字标签经常不忍直视。最常见的处理是手动设置刻度axisX-setTickCount(10); axisX-setMinorTickCount(5); axisX-setLabelFormat(%.1f); axisY-setRange(0, 100); axisY-applyNiceNumbers();applyNiceNumbers()是我特别推荐的一个函数它会自动把 Y 轴的刻度修整成“漂亮”的整数比如 0、20、40、60而不是 0、17.3、34.6 这种。做数据看板时美观度提升非常明显。如果 X 轴分类标签太长显示不下可以考虑旋转标签axisX-setLabelsAngle(-45);或者用QBarCategoryAxis的标签换行。这个细节在柱状图里很常见不处理的话文字会叠在一起。6.3 中文乱码和坐标轴标题不显示Qt5 以后默认 Unicode正常不会乱码。如果你遇到中文乱码先检查源文件编码是不是 UTF-8然后字符串都用QStringLiteral包起来。不要自己用QTextCodec去瞎转转着转着就乱了。坐标轴标题不显示常见原因是轴太短、字体太小或者 title 默认不可见。可以先确认标题名axisX-setTitleText(QStringLiteral(时间)); axisX-setTitleVisible(true); axisX-setTitleFont(QFont(QStringLiteral(Microsoft YaHei), 10));注意轴标题默认会给一定的留白如果图表区域太小标题可能被挤到看不见。这种情况下要调大图表 margins 或者窗口高度。6.4 常见编译和运行问题速查这里把我踩过的一些问题整理成清单照着排查能省不少时间。编译过了但运行崩溃查看series是否在栈上创建然后又被 chart 接管建议统一用 new 创建。createDefaultAxes()之后自定义轴不生效createDefaultAxes()会创建默认轴并存到 chart 里如果你又手动addAxis同一个方向需要先把默认轴移除或者干脆别用createDefaultAxes()。找不到QChart名称记得#include QtCharts/QChart并且using namespace QtCharts;或QT_CHARTS_USE_NAMESPACE。CMake 找不到 Charts确认安装了 Qt Charts 模块CMake 用find_package(Qt5Charts REQUIRED)。删除 series 后 crash先chart-removeSeries(series)再delete series不要直接 delete 一个还在 chart 里的对象。7. 项目里别裸写一个轻量 ChartWidget 封装思路7.1 为什么我建议做封装如果你只是写一个演示程序直接在界面里 new 一个 QChartView 没有任何问题。但项目里图表通常会在多个页面里重复出现而且业务方今天要加一条曲线、明天要改一个颜色每次都在业务代码里到处改 chart 操作维护成本会迅速膨胀。我建议把图表的创建、坐标轴配置、数据更新、导出、清空这些动作收敛到一个类里。封装还有一个隐藏好处如果以后真要把 QtChart 换成 QCustomPlot业务层只需要面对同一个接口内部实现改掉就行界面代码不用动。这个解耦价值在中期项目里会体现得非常明显。7.2 接口长这样我常用的一套接口设计是这样的class ChartWidget : public QWidget { Q_OBJECT public: explicit ChartWidget(QWidget *parent nullptr); void addLineSeries(const QString name, const QColor color); void updateLineData(const QString name, const QVectorQPointF points); void appendLinePoint(const QString name, const QPointF point); void clearAllSeries(); void setChartTitle(const QString title); void exportChart(const QString filePath); private: QChartView *m_chartView nullptr; QChart *m_chart nullptr; QValueAxis *m_axisX nullptr; QValueAxis *m_axisY nullptr; QHashQString, QLineSeries * m_seriesMap; QListQColor m_colorPalette; };对外暴露的不是 QLineSeries 指针而是曲线名字。业务层传数据是chartWidget-updateLineData(QStringLiteral(CPU), points);不用关心内部是 QLineSeries 还是 QScatterSeries也不用关心坐标轴怎么配。这个设计在多人协作项目里特别有用。7.3 实现细节和自动配色在构造函数里我会统一创建 chart、chartView、坐标轴并设置好主题和抗锯齿。addLineSeries负责从调色板取色、创建 series、写入m_seriesMap并且 attach 到公共轴。自动配色用一个固定颜色表循环即可m_colorPalette QColor(#5470c6) QColor(#91cc75) QColor(#fac858) QColor(#ee6666) QColor(#73c0de);取色逻辑可以简单一点int index m_seriesMap.size() % m_colorPalette.size(); QColor color m_colorPalette.at(index); series-setColor(color);updateLineData的核心就是找到对应 series然后调replace(points)。如果 series 不存在可以自动调用addLineSeries这样业务方在初始化时少写一个流程。clearAllSeries要清空数据但保留坐标轴for (QLineSeries *series : qAsConst(m_seriesMap)) { series-clear(); }不要每次都把 axis 也删了因为坐标轴配置通常是稳定的。这段封装我在两个项目里改过三版最大的体会是第一次写图表代码时不要把图表对象散落在业务类里否则后期每个界面的轴配置都会翻车。把“数据更新”作为唯一入口内部负责加轴、配色、清理这是最省心的做法。QtChart 本身不是万能的真到了海量数据或者复杂交互的时候该换 QCustomPlot 也不要硬撑。但从二维监控图和统计报表这个范围来看先用它把业务跑通性价比非常高。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑