QCustomPlot 多坐标系实战:双Y轴与多轴矩形排布全解析

发布时间:2026/10/14 19:01:12
QCustomPlot 多坐标系实战:双Y轴与多轴矩形排布全解析
简介这是一份基于Qt与QCustomPlot实现多坐标系绘图的完整工程资源适合需要掌握二维图表高级布局、多轴关联与交互控制的Qt开发者学习参考。压缩包共10个文件包含4个cpp、3个h、1个ui及工程配置文件涵盖qcustomplot库源码、主界面、widget逻辑与示例项目文件可清晰还原一个可运行的多坐标系示例。目前已有3880人学习下载。资源围绕QCPAxisRect实现多坐标系创建、动态添加坐标系、共享X轴以及双击切换坐标系颜色等关键操作并附有完整工程代码与界面设计便于读者直接编译查看或提取改造到自身项目中。通过学习该工程可以较快掌握QCustomPlot在复杂数据可视化场景中的配置思路与二次开发技巧。1. 一个界面上要显示三种量级的数据逼我把 QCustomPlot 多坐标系吃透做数据采集工具时碰到过一个典型需求同一根时间轴上要显示千伏级母线电压、毫伏级振动信号和只有几十摄氏度级的绕组温度。最开始我把三条曲线硬塞进同一个坐标系结果振动和温度曲线直接被压成贴在零轴上的两条平线什么信息都读不出来。这才意识到 QCustomPlot 多坐标系不是“多画几条线”那么简单它要处理的是轴矩形QCPAxisRect、轴QCPAxis和图元绑定三层关系让不同量纲、不同量级的数据各自拥有独立的量程。这篇笔记会从坐标系模型讲起拆双 Y 轴和多轴矩形两套主流实现再把我实际项目里反复踩过的坑和排查顺序整理出来。适合已经能用 QCustomPlot 画出第一条曲线、一碰到多量纲数据就不知道如何排布的读者。2. 坐标系的三层结构轴矩形、轴和图元绑定QCustomPlot 看起来只是一个绘图控件但它的内部是一张布局网格坐标系不是控件的一个属性而是由“轴矩形 轴 图元绑定”拼出来的。很多人第一次做多坐标系失败就是因为只盯着 QCPGraph 看忽略了另外两层。所以我建议先花几分钟把模型理清楚再动手写代码后面对参数和排错都轻松得多。2.1 轴矩形是坐标系的实体容器布局网格是挂载点默认情况下customPlot-plotLayout()是一个 QCPLayoutGrid第 0 行第 0 列放着一个 QCPAxisRect也就是customPlot-axisRect(0)。这个轴矩形自带四条轴xAxis下、yAxis左、xAxis2上、yAxis2右其中 xAxis2 和 yAxis2 默认隐藏。“多坐标系”的第一层含义就是在这个布局网格里追加新的轴矩形或者在同一个轴矩形里用addAxis()追加新轴。写代码之前先跑一段诊断程序把当前控件里到底有几个坐标系、每个坐标系挂了哪些轴看明白。这条逻辑我在排查布局问题时几乎每次都用// 列出当前绘图控件里所有轴矩形及其轴排查多坐标系布局用 QCPLayoutGrid *layout customPlot-plotLayout(); for (int i 0; i layout-elementCount(); i) { QCPAxisRect *rect qobject_castQCPAxisRect *(layout-elementAt(i)); if (!rect) continue; // 布局格里可能放的是图例等其他元素 qDebug() axisRect i 底部轴: rect-axis(QCPAxis::atBottom) 左侧轴: rect-axis(QCPAxis::atLeft) 轴总数: rect-axes().size(); }这里elementAt(i)按布局顺序取子元素qobject_cast是为了跳过图例这类非轴矩形元素新建的轴矩形默认也是四条轴所以axes().size()初始是 4每addAxis一次加 1。rect-axis(QCPAxis::atBottom)的第二个参数是序号0 表示该方向上第一条轴追加出来的轴从序号 1 开始。这组 API 是 2.x 的写法1.x 大体兼容但交互配置差别很大后面第 5 章会专门讲。2.2 图元绑定一条曲线属于哪个坐标系由 key 轴和 value 轴决定坐标系有了容器和轴还得让数据住进去。QCustomPlot 里所有图元QCPAbstractPlottable都持有两个轴指针key 轴通常就是 X 轴和 value 轴通常就是 Y 轴。一条曲线画在哪个坐标系完全由这两个指针决定和数据本身无关。因此创建图元时最稳的入口是addGraph(keyAxis, valueAxis)把坐标系的“身份证”一次传到位。不传参时图元默认绑到第一个轴矩形的 xAxis/yAxis这正是多坐标系里最常见的误用来源。如果已经用默认方式建了图事后也能换绑数据不需要重新填QCPGraph *g customPlot-addGraph(); // 默认绑定第一个矩形的 xAxis/yAxis // 数据不变只换坐标系改绑到第二个矩形的轴 g-setKeyAxis(rect2-axis(QCPAxis::atBottom)); g-setValueAxis(rect2-axis(QCPAxis::atLeft)); customPlot-replot();setKeyAxis会立刻按新坐标系的映射重排图元但不会改动数据数据和坐标系是解耦的。最后那句replot()在数据量小的时候可以每次调量大的场景建议先把一批数据全部 set 完、做一次统一刷新否则 60 Hz 刷新的界面会肉眼可见地卡。2.3 四条默认轴的归属副轴不是新坐标系选型别选错xAxis2 和 yAxis2 属于第一个轴矩形。把 yAxis2 设成可见、给个范围只是在同一个坐标系右侧加了刻度不会产生第二个独立坐标系要真正造出第二个坐标系要么addAxis追加轴要么新建 QCPAxisRect 挂到布局里。这个区分直接决定需求选型我一般按下面的表来定需求特征选型理由同一个时间轴量级差几个数量级同一轴矩形追加右轴双 Y 轴共享 X 交互和范围同一个 X但一个要线性一个要对数两个轴矩形轴级 scaleType 各自独立时间基线、采样率不同两个轴矩形各自 X 范围互不干扰只是想多显示一组同量级读数yAxis2 设可见即可不加坐标系最小改动选错的下场是后期大量返工量级不同选了副轴缩放联动会让你怀疑人生时间基线不同选了双 Y 轴X 轴范围永远没法两边兼顾。所以动手前先拿这个表过一遍需求。3. 双 Y 轴实现同一坐标系里开第二个 Y 轴代码和参数怎么给双 Y 轴是“多坐标系”里需求密度最高的形态一个绘图区、两个量程视觉上最节省空间代码也最短。适合两个量级相差很大、但时间轴完全一致的数据。实现要点是三条右轴颜色与曲线绑定、X 轴范围手动合成、交互只开水平方向。3.1 最小代码addAxis 加右轴addGraph 显式绑定直接给一套能跑的最小实现模拟场景是母线电压千伏级和振动信号毫伏级同屏显示// QCustomPlot 2.x 写法1.x 大体兼容 QCustomPlot *p ui-plot; // 1) 在默认轴矩形右侧追加第二条 Y 轴 QCPAxis *yRight p-axisRect()-addAxis(QCPAxis::atRight); yRight-setLabel(振动 (mV)); yRight-setRange(-100, 100); // 先给业务量程别裸奔 yRight-setPen(QPen(QColor(#e07b39))); // 轴颜色与曲线保持一致 yRight-setTickLabelColor(QColor(#e07b39)); yRight-setLabelColor(QColor(#e07b39)); // 2) 两条曲线分别绑定左轴和右轴key 轴共用 xAxis QCPGraph *volt p-addGraph(p-xAxis, p-yAxis); volt-setPen(QPen(QColor(#3b6ea5))); QCPGraph *vib p-addGraph(p-xAxis, yRight); vib-setPen(QPen(QColor(#e07b39))); // 3) 喂数据1000 点示意 QVectordouble t(1000), u(1000), v(1000); for (int i 0; i 1000; i) { t[i] i * 0.01; u[i] 320.0 10.0 * qSin(i * 0.02); v[i] 20.0 * qSin(i * 0.05); // 毫伏级 } volt-setData(t, u); vib-setData(t, v); // 4) 分轴缩放共享 X 轴手动合成左右 Y 轴按各自曲线收紧 p-xAxis-setRange(t.first(), t.last()); volt-rescaleValueAxis(false); vib-rescaleValueAxis(false); p-replot();addAxis(QCPAxis::atRight)的返回值就是新轴后续所有设置都作用在它上面颜色三件套轴、刻度标签、轴名统一用曲线色是读图的视觉锚点。yRight-setRange(-100, 100)这一步很多人省略结果数据一刷新轴就跟着跳图形忽大忽小所以业务上下限能写死就写死。rescaleValueAxis(false)的 false 表示在数据包络外留少量边距视觉上更宽松true 是严格贴边。注意这里 X 轴我故意没用rescaleKeyAxis因为两条曲线共享 key 轴逐个缩放的先后顺序会互相覆盖手动合成范围最稳。3.2 图例与可视化绑定让右轴和曲线看着是一伙的双轴图最怕读者分不清哪条线属于哪个轴。除了颜色统一图例也要明确给出单位volt-setName(母线电压 (kV)); vib-setName(振动 (mV)); p-legend-setVisible(true); p-legend-setBrush(QBrush(QColor(255, 255, 255, 230)));图例默认会在布局里占一格多坐标系场景下它可能与轴矩形抢空间尤其右侧加了 Y 轴后。我一般把图例字体调小一档或者用setFillOrder控制换行方向如果右侧 Y 轴刻度比较宽图例干脆放左上角避免和右轴重叠。3.3 双 Y 轴的交互参数默认只开水平双 Y 轴场景下垂直方向有左右两套量级竖直拖动/缩放会让两条曲线一起乱飞所以交互配置建议只开水平p-axisRect()-setRangeDrag(Qt::Horizontal); p-axisRect()-setRangeZoom(Qt::Horizontal);第一个轴矩形的setRangeDrag在 2.x 里是作用在 QCPAxisRect 上的老项目里写在customPlot-setRangeDrag上的代码迁移过来要改位置否则编译报错或者静默失效。水平拖动时左右两个 Y 轴都不动只有 X 范围变化两条曲线的相对关系始终保持这是双 Y 轴最舒服的浏览方式。4. 上下堆叠轴矩形真正意义上的多坐标系布局双 Y 轴适合“同一个时间轴、两个量级”。但如果一路是秒级采样、一路是小时级基线或者一个要看线性刻度一个要看对数刻度就不要硬挤在一个矩形里了。把坐标系拆成独立的 QCPAxisRect上下堆叠排列是 QCustomPlot 多坐标系的正路。4.1 在布局网格里插入第二个 QCPAxisRect// 1) 新建轴矩形并挂到布局第二行 QCPAxisRect *rect2 new QCPAxisRect(customPlot); rect2-axis(QCPAxis::atLeft)-setLabel(功率 (dBm)); customPlot-plotLayout()-addElement(1, 0, rect2); // 第1行第0列行列从0起 // 2) 两个矩形之间的行间距 customPlot-plotLayout()-setRowSpacing(10);默认矩形占据 (0, 0)addElement(1, 0, rect2)之后两个矩形上下排开。这里有个新手常忽略的点QCPAxisRect 构造后不会自动进布局只挂在对象树里必须调用addElement才会显示。新矩形自带四条轴默认显示 left 和 bottom只想显示单 Y 轴时把不需要的轴setVisible(false)即可。setRowSpacing(10)控制两张图之间的留白别设太小否则上方 Y 轴的刻度标签会贴到下方图的顶边上视觉上很难受。4.2 把图元和交互分别指到对应矩形// 图元创建时直接指定新矩形的轴 QCPGraph *spec customPlot-addGraph(rect2-axis(QCPAxis::atBottom), rect2-axis(QCPAxis::atLeft)); spec-setData(freq, power); // 交互按矩形分开配 rect2-setRangeDrag(Qt::Horizontal); rect2-setRangeZoom(Qt::Horizontal); // 第一个矩形保持默认用 customPlot-axisRect(0) 单独设置addGraph两个参数就是图元的坐标系身份证漏传就会默认进第一个矩形这是多轴矩形场景出现“曲线跑到别人家”的根本原因。交互配置同样按矩形独立想让上下两张图各自缩放平移就给各自setRangeDrag如果副矩形只是给个全局视角完全不用配交互鼠标落在它上面不会有任何动作反而干净。4.3 边距对齐用 QCPMarginGroup 让两张图的绘图区宽度一致两张图上下排好后最容易暴露的问题就是 X 轴网格线对不齐左边 Y 轴刻度位数不同一个是 5.2一个是 -13.4导致绘图区宽度不一致。这个问题的解决不是靠调坐标而是靠边距分组QCPMarginGroup *mg new QCPMarginGroup(customPlot); customPlot-axisRect(0)-setMarginGroup(QCP::msLeft | QCP::msRight, mg); rect2-setMarginGroup(QCP::msLeft | QCP::msRight, mg);setMarginGroup会强制组内所有轴矩形的指定边距取最大值左右对齐后两个绘图区等宽网格线自然贯通。参数上只对 msLeft 和 msRight 分组就够了顶部底部不分组因为要给各自轴名和刻度留独立空间。对齐这件事有时候很玄学间隙差几个像素看不出来但截图和打印时特别明显所以我一上来就把 margin group 加上不带犹豫的。4.4 时间基线一致时副矩形的 X 轴建议藏刻度如果两张图的时间基线完全一致只是量纲不同副矩形底部再显示一套 X 轴刻度是纯粹的视觉冗余。把rect2-axis(QCPAxis::atBottom)-setVisible(false)藏掉和上方矩形自然形成“共用 X 轴”的观感。注意隐藏的是刻度显示图元绑定关系不受影响数据照常画。藏完之后记得用 margin group 保证两个矩形左右仍然对齐这一步不做隐藏副轴刻度后左右错位会更刺眼。5. 多坐标系避坑指南五个高频翻车现场与排查顺序多坐标系的问题几乎都出在“绑定”和“范围”两个字上。下面是五个我实际项目里遇到过、也帮别人排查过的翻车现场按现象、原因、解决的顺序写排错时可以照着过一遍。5.1 曲线越狱新加的右轴空空如也曲线全挤在默认轴现象用axisRect()-addAxis(QCPAxis::atRight)加了右轴轴也显示了但新曲线还是画在左轴上右轴上一根线都没有。原因addGraph()没传轴参数图元默认绑到第一个轴矩形的 xAxis/yAxis加轴只是加了坐标系外壳没有把图元迁进去。解决创建图元时用addGraph(p-xAxis, yRight)显式绑定或者在创建后用setKeyAxis/setValueAxis换绑。我的习惯是前者一次到位事后换绑容易漏掉某条线。排查时先qDebug()打一下图元的keyAxis()和valueAxis()指向对比一下就能定位。5.2 rescaleAxes() 一梭子下去两个坐标系的量程全乱现象双 Y 轴图刚调好某条曲线被压扁或者直接跑出视野手动设过的业务量程也会被冲掉。原因rescaleAxes()会遍历所有图元逐个对轴缩放。共享同一个轴的时候后处理的图元会覆盖先处理的范围两个 value 轴虽然独立但只要有一个图元的范围异常整张图的比例就崩了。解决共享轴比如双 Y 轴里的 X手动合成范围用xAxis-setRange(min, max)写死每个 value 轴用各自图元的rescaleValueAxis(false)单独收紧。需要严格业务边界时干脆setRange一个自字写死不要依赖自动缩放。5.3 第二个坐标系建了却不显示现象new QCPAxisRect执行了图元也绑了界面上就是看不到第二张图布局似乎没变化。原因QCPAxisRect 构造后不会自动进入布局网格必须plotLayout()-addElement(row, col, rect)挂载或者行列参数给错和已有元素挤在同一格。解决创建后立刻执行plotLayout()-addElement(1, 0, rect2)然后用第 2.1 节那段诊断代码遍历一遍布局确认elementAt(i)里能拿到它。如果拿到的是别的元素检查行列号是否被图例或其他元素占用。5.4 上下两张图的 X 轴网格线错开几个像素现象堆叠布局里两张图各自的 X 轴刻度对不上竖着看网格线是断的导出图片时尤其明显。原因左侧 Y 轴的刻度标签宽度不同导致两个轴矩形的绘图区宽度不一致这不是坐标问题是边距问题。解决用 QCPMarginGroup 把两个矩形的 msLeft / msRight 分组见 4.3如果还不够齐检查两张图 Y 轴刻度字号、轴名长度尽量统一。注意 margin group 要在两个矩形都创建完成后再设置顺序反了不生效。5.5 老代码 setRangeDrag 拖不动交互静默失效现象从 1.x 老代码迁移过来的项目customPlot-setRangeDrag(Qt::Horizontal)编译能过但鼠标拖不动图或者拖的是第一个矩形、副矩形纹丝不动。原因2.x 把拖拽缩放交互配置迁移到了 QCPAxisRect 上老的全局快捷设置行为和目标都不对了多矩形场景下每个矩形的交互本来就是独立的。解决对每个 QCPAxisRect 分别调用setRangeDrag/setRangeZoom并确认setInteractions(QCP::iRangeDrag | QCP::iRangeZoom)已开启总开关。排查顺序建议是先查总开关再查目标矩形最后查是不是有别的元素如图例挡住了鼠标事件。6. 进阶手法多坐标系下的 X 轴联动、对数轴混排与出图检查多坐标系做到能显示只是及格能让多个坐标系协同工作才是实战水平。这里给三个我常用的进阶手法。6.1 主从 X 轴联动一个矩形拖动其他矩形跟着走堆叠两三个坐标系后用户拖动主矩形看细节希望副矩形的 X 范围同步平移否则上下两张图瞬间脱节。一行连接就能实现// 从矩形跟随主矩形保持多坐标系同一条时间基线 connect(customPlot-axisRect(0)-axis(QCPAxis::atBottom), qOverloadconst QCPRange (QCPAxis::rangeChanged), rect2-axis(QCPAxis::atBottom), qOverloadconst QCPRange (QCPAxis::setRange));注意这是单向主从联动。双向联动需要在槽函数里做防抖否则两个轴互相发 setRange 会来回触发。我一般只做主从副矩形定位成“全景视角”避免交互复杂度失控。6.2 对数轴与线性轴混排多坐标系最常见的进阶需求是一个矩形看线性趋势、另一个矩形看对数频谱。轴级scaleType是独立属性所以混排没有任何额外成本rect2-axis(QCPAxis::atLeft)-setScaleType(QCPAxis::stLogarithmic); rect2-axis(QCPAxis::atLeft)-setScaleLogBase(10);唯一要注意的是对数轴数据必须全为正喂数据前先过滤 0 和负值否则 rescale 结果会异常。我习惯在数据入口统一做一次合法性检查而不是等绘图时再排雷。6.3 出图前检查清单导出图片或交付前我按这张表过一遍每次都靠它拦下低级问题检查项期望结果各轴颜色与对应曲线一致一眼能分出坐标系归属主从 X 联动开关符合交互预期拖动主矩形副矩形同步跟随图例没有遮挡右侧轴或副矩形布局内无重叠两张图绘图区左右对齐网格线竖看贯通我自己在这个方向上最大的教训就是别等界面全部排好再验证坐标映射是否正确而是每挂一个轴矩形、每绑定一条曲线就用第 2.1 节的诊断代码打一次轴归属。多坐标系问题大多在最后一刻集中爆发定期探测一次绑定关系能帮你省掉大半排错时间。这套排法和选型表我用了好几个模拟采集项目希望帮到你。本文还有配套的精品资源点击获取