深入浅出(十三)QCustomPlot库—高性能二维绘图 QCustomPlot库——高性能二维绘图1. QCustomPlot简介1.1 QCustomPlot下载1.2 QCustomPlot特性2. QCustomPlot基本结构3. QCustomPlot基本使用示例2.0 CMake 示例2.1 创建 QCustomPlot 控件2.2 绘制一条最简单的曲线2.3 常见图表类型2.4 交互操作2.5 标注、十字线和图层特性2.6 性能优化经验附录一 常用功能和踩坑1. 右上角图例的隐藏与显示2. QCustomPlot 自身崩溃问题——诱因是缩放拖拽3. QCustomPlot 自适应调节1. QCustomPlot简介在 Qt 桌面应用开发中二维数据可视化几乎是绕不开的需求实时曲线传感器、设备状态离线数据分析CSV、日志、测试结果工业/科研软件中的趋势图、统计图1.1 QCustomPlot下载源说明官网地址https://www.qcustomplot.comGitHub地址https://github.com/ET-BE/QCustomPlot官方文档https://www.qcustomplot.com/documentation1.2 QCustomPlot特性Qt 自带的 QPainter 足够底层但开发成本高Qt Charts 使用方便但在性能和可定制性上存在一定限制。QCustomPlot 正好处在两者之间轻量、开源、单头文件qcustomplot.hqcustomplot.cpp基于 QWidget与 Qt 原生生态高度融合在高刷新率、大数据量场景下依然表现优秀本文将从 原理、使用方法、常见图表、交互与性能优化 等方面系统介绍 QCustomPlot 的实际用法。2. QCustomPlot基本结构顶层结构QCustomPlot └── QCPAxisRect 画图区 ├── xAxis 下 ├── yAxis 左 ├── xAxis2 上 └── yAxis2 右这 4 条轴是“固定槽位”不是动态生成的。默认形态plot-xAxis// 可见plot-yAxis// 可见plot-xAxis2// 不可见plot-yAxis2// 不可见3. QCustomPlot基本使用示例2.0 CMake 示例add_executable(MyApp main.cpp mainwindow.cpp qcustomplot.h qcustomplot.cpp )2.1 创建 QCustomPlot 控件QCustomPlot*plotnewQCustomPlot(this);setCentralWidget(plot);2.2 绘制一条最简单的曲线QVectordoublex(100),y(100);for(inti0;i100;i){x[i]i;y[i]qSin(i*0.1);}plot-addGraph();plot-graph(0)-setData(x,y);plot-xAxis-setLabel(X);plot-yAxis-setLabel(Y);plot-rescaleAxes();plot-replot(); 关键点addGraph()添加一条曲线setData()一次性设置数据rescaleAxes()自动缩放坐标轴2.3 常见图表类型折线 / 散点图plot-graph(0)-setLineStyle(QCPGraph::lsLine);plot-graph(0)-setScatterStyle(QCPScatterStyle::ssCircle);柱状图QCPBarsQCPBars*barsnewQCPBars(plot-xAxis,plot-yAxis);bars-setWidth(0.6);bars-setData(keys,values);多 Y 轴plot-yAxis2-setVisible(true);plot-addGraph(plot-xAxis,plot-yAxis2);适用于不同量纲数据左右轴对比2.4 交互操作启用拖拽 缩放plot-setInteractions(QCP::iRangeDrag|QCP::iRangeZoom);轴联动缩放connect(plot-xAxis,SIGNAL(rangeChanged(QCPRange)),plot-xAxis2,SLOT(setRange(QCPRange)));曲线选中plot-setInteractions(QCP::iSelectPlottables);2.5 标注、十字线和图层特性Item 系统标注、十字线QCPItemText*textnewQCPItemText(plot);text-position-setCoords(5,0.8);text-setText(Peak);常用于峰值标注关键点说明图层Layerplot-addLayer(overlay);item-setLayer(overlay);用于控制绘制顺序。2.6 性能优化经验批量更新数据QVectordoublex,y;graph-setData(x,y);避免逐点 addData()。降低刷新频率plot-replot(QCustomPlot::rpQueuedReplot);大数据量绘制关闭抗锯齿使用 setAdaptiveSampling(true)plot-setNotAntialiasedElements(QCP::aeAll);附录一 常用功能和踩坑1. 右上角图例的隐藏与显示在工程中经常碰到因功能的切换导致绘图数据的变化同时更新右上角图例的问题可采用以下代码进行操作。只保留第一个右上角图例去除其余折线图例intsizecustomPlot1-graphCount();for(inti1;isize;i){customPlot1-graph(i)-removeFromLegend();}通过以上方法去除图例后必须通过一下代码添加图例才能显示customPlot1-graph(1)-setName(拟合线);customPlot1-graph(1)-addToLegend();// 关键2. QCustomPlot 自身崩溃问题——诱因是缩放拖拽QCustomPlot 有个非常坑爹的Bug就是你在拖拽或者缩放的时候没有任何限制会导致软件直接崩溃比如无限缩小或者移动到实际数据Rang之外都会导致崩溃。以下是解决办法绑定QCustomPlot对象的rangeChanged信号如下图所示connect(customPlot1-xAxis,qOverloadconstQCPRange(QCPAxis::rangeChanged),this,[this](constQCPRangerange){CustomPlotRangeUpdate(customPlot1,range,0);});槽函数限制缩放拖拽的Range如下示例#defineAXIS_MIN-1000000.0#defineAXIS_MAX1000000.0#defineAXIS_MIN_SPAN1e-6// 最小缩放宽度防止无限缩小/** * brief 对 QCustomPlot 的坐标轴范围进行安全限制与修正。 * * 主要作用 * 1. 限制坐标轴显示范围在预设的最小 / 最大边界内AXIS_MIN ~ AXIS_MAX * 2. 防止因缩放、拖拽导致的非法区间NaN / Inf / 反向区间 * 3. 防止坐标轴被无限缩小最小跨度保护 * 4. 当范围越界时自动回拉并保持当前缩放比例不变 * * 典型使用场景 * - 鼠标滚轮缩放 * - 拖拽平移 * - 动态数据刷新后限制视野 * * 注意事项 * - 本函数仅负责修正坐标轴范围不主动调用 replot() * 建议在外部统一触发 replot避免频繁刷新导致卡顿 * - 适合在 QCPAxis::rangeChanged 信号中调用 * * param customPlot QCustomPlot 指针不能为空 * param range 当前坐标轴请求的显示范围 * param xAxisFlag true 表示限制 X 轴 * false 表示限制 Y 轴 */voidCustomPlotRangeUpdate(QCustomPlot*customPlot,constQCPRangerange,boolxAxisFlag){if(!customPlot)return;// 1. 基本合法性检查if(!std::isfinite(range.lower)||!std::isfinite(range.upper))return;QCPRange adjustedRangerange;// 2. 处理反向区间if(adjustedRange.loweradjustedRange.upper)std::swap(adjustedRange.lower,adjustedRange.upper);doublespanadjustedRange.size();// 3. 防止 0 / 负 spanif(!std::isfinite(span)||span0)spanAXIS_MIN_SPAN;// 4. 限制最小缩放比例if(spanAXIS_MIN_SPAN)spanAXIS_MIN_SPAN;boolneedAdjustfalse;// 5. 下边界限制if(adjustedRange.lowerAXIS_MIN){adjustedRange.lowerAXIS_MIN;adjustedRange.upperAXIS_MINspan;needAdjusttrue;}// 6. 上边界限制if(adjustedRange.upperAXIS_MAX){adjustedRange.upperAXIS_MAX;adjustedRange.lowerAXIS_MAX-span;needAdjusttrue;}// 7. 应用范围仅当发生变化if(needAdjust){if(xAxisFlag){customPlot-xAxis-setRange(adjustedRange);}else{customPlot-yAxis-setRange(adjustedRange);}customPlot-replot();// 注意尽量不在这里 replot避免抖动 性能问题}}3. QCustomPlot 自适应调节/** * brief 判断一个 QCPRange 是否为有效的一维坐标范围。 * param r 待检测的坐标范围QCPRange * return true 范围有效可安全用于 setRange false 范围非法不应直接使用 */inlineboolisValidRange1x(constQCPRanger){returnstd::isfinite(r.lower)std::isfinite(r.upper)r.upperr.lower;}/** * brief 根据当前图表数据自适应更新 QCustomPlot 的坐标轴显示范围。 * * 通常用于 * - 实时曲线 * - 数据更新后自动缩放视图 * - 用户缩放受限 自动回弹的场景 * * param plot 目标 QCustomPlot 对象 * param xAdaptive 是否对 X 轴启用自适应范围 * param yAdaptive 是否对 Y 轴启用自适应范围 */#defineAdaptiveRatio0.3voidCustomPlotAdaptiveRange(QCustomPlot*plot,boolxAdaptivefalse,boolyAdaptivefalse){if(!plot||plot-graphCount()0)return;QVectorQCPRangevalidXranges;QVectorQCPRangevalidYLeftRanges;QVectorQCPRangevalidYRightRanges;// 1️.收集所有有效 graph 的范围for(inti0;iplot-graphCount();i){QCPGraph*graphplot-graph(i);if(!graph||graph-data()-isEmpty())continue;boolokKeyfalse,okValuefalse;QCPRange xRangegraph-getKeyRange(okKey);QCPRange yRangegraph-getValueRange(okValue);if(!okKey||!okValue||!isValidRange1x(xRange)||!isValidRange1x(yRange)){continue;}validXranges.append(xRange);if(graph-valueAxis()plot-yAxis)validYLeftRanges.append(yRange);elseif(graph-valueAxis()plot-yAxis2)validYRightRanges.append(yRange);}if(validXranges.isEmpty())return;// 2️.X 轴联合范围QCPRange combinedXvalidXranges.first();for(inti1;ivalidXranges.size();i)combinedX.expand(validXranges[i]);if(!isValidRange1x(combinedX))return;constdoublexMargincombinedX.size()*AdaptiveRatio;plot-xAxis-setRange(xAdaptive?(combinedX.lower-xMargin):0.0,combinedX.upperxMargin);// 3️.左 Y 轴if(!validYLeftRanges.isEmpty()){QCPRange yRangevalidYLeftRanges.first();for(inti1;ivalidYLeftRanges.size();i)yRange.expand(validYLeftRanges[i]);if(isValidRange1x(yRange)){constdoubleyMarginyRange.size()*AdaptiveRatio;plot-yAxis-setRange(yAdaptive?(yRange.lower-yMargin):0.0,yRange.upperyMargin);}}// 4️.右 Y 轴if(plot-yAxis2plot-yAxis2-visible()!validYRightRanges.isEmpty()){QCPRange yRangevalidYRightRanges.first();for(inti1;ivalidYRightRanges.size();i)yRange.expand(validYRightRanges[i]);if(isValidRange1x(yRange)){constdoubleyMarginyRange.size()*AdaptiveRatio;plot-yAxis2-setRange(yAdaptive?(yRange.lower-yMargin):0.0,yRange.upperyMargin);}}plot-replot(QCustomPlot::rpQueuedReplot);}