QCustomPlot多坐标系实战:轴的绑定、布局与联动避坑指南
发布时间:2026/10/14 3:09:56 锦皓数字建站

简介面向需要掌握 QCustomPlot 高级绘图能力的 Qt 开发者这份代码工程围绕多坐标系核心机制展开完整演示了如何通过 QCPAxisRect 创建多个坐标系、动态添加坐标轴区域、实现共享 X 轴联动以及利用双击事件切换坐标系颜色。压缩包共 10 个文件以 4 个 cpp 源文件与 3 个 h 头文件为主附带 ui 界面设计文件与 pro 工程文件整体仅 241KB结构精简便于直接打开工程对照学习。作为一份可运行的示例项目它展示了多坐标系布局的关键 API 调用流程读者可从中获得坐标轴矩形管理、轴联动配置、双击事件交互处理等实用代码范式对于需要在一张图表中同时呈现不同尺度数据、对比多组曲线的场景极具参考价值。已有 3880 人学习下载适合希望在项目中快速搭建复杂 2D 图表界面的中高级 Qt 开发者。1. 多坐标系不是多画几条曲线而是把轴的归属想清楚做数据采集上位机的人大概率遇到过这种场景同一块面板里要同时显示温度和压力温度在 0 到 100 之间波动压力轻松上到几千帕。如果两条曲线挂在同一条 Y 轴上小温度曲线会被压成一条平线毛细节都看不到。这时候直觉反应是「用 QCustomPlot 的多坐标系」。qcustomplot 多坐标系真正的含义不是把一个控件里塞多个绘图区而是搞清楚轴与图元之间的绑定关系让每条曲线挂到属于它自己的坐标系上。本文会把这些轴的创建、绑定、布局和联动讲清楚适合正在做仪器仪表、嵌入式上位机、数据可视化面板的 Qt 开发者。2. 坐标系是怎么搭起来的axisRect、轴与曲线的绑定关系2.1 一个 Plot 能装几个坐标系从 layout 这张表开始数QCustomPlot 里整个绘图控件叫 QCustomPlot它内部不是一个直接画图的面板而是有一个排版系统 plotLayout。plotLayout 是一个网格布局默认只有一行一列里面放着一个 QCPAxisRect。每次你在代码里直接调用plot-addGraph()然后往默认轴上 setData数据最终都落到这同一个 axisRect 上。一个坐标系的最小单位不是 QCustomPlot而是 QCPAxisRect。一个 QCPAxisRect 可以理解为一块矩形的绘图区域它自带四条轴线上、下、左、右。默认情况下只有下轴xAxis和左轴yAxis是可见的上轴和右轴虽然对象存在但默认不显示刻度也不显示轴线。所以「一个 Plot 里能装几个坐标系」这个问题的答案很明确数量上没有硬限制你可以在 plotLayout 里塞多个 QCPAxisRect每个 rect 都有自己的四条轴和独立的刻度范围即使只用一个 rect你也可以通过开启右侧轴来容纳第二条 Y 轴数据。前者叫多绘图区后者叫单绘图区多轴两者都是多坐标系应用里的常见形态。我一般建议按数据的物理意义来选如果两组数据的横轴完全相同只是纵轴量级不同优先用同一个 axisRect 加右侧 Y 轴如果横轴含义都不同比如上面是时域波形、下面是频谱就应该拆成两个 axisRect。这样布局上更容易对齐图例和网格也能各自独立。2.2 曲线跟轴走还是轴跟曲线走keyAxis 与 valueAxis 的绑定逻辑很多新手在这里绕晕明明是往一个 Plot 里 addGraph为什么曲线会出现在另一个坐标系里原因是 QCustomPlot 里图元QCPGraph不是直接挂在 axisRect 上的而是挂在轴上。每条 graph 有两个重要的轴指针keyAxis 表示横轴valueAxis 表示纵轴。默认情况下 addGraph 创建的 graphkeyAxis 指向主 rect 的下轴valueAxis 指向主 rect 的左轴。如果你新建了一个 QCPAxisRect也新建了它内部的轴但忘记把 graph 的 keyAxis 和 valueAxis 指过去那么这条曲线还是画在默认坐标系上。这不是 QCustomPlot 自动帮你识别「哪个坐标系合适」它没有这个智能绑定关系完全靠你自己维护。另外还要注意一条曲线可以横轴用 A 坐标系的轴、纵轴用 B 坐标系的轴这在语法上是合法的。比如你把 keyAxis 设为上图的下轴valueAxis 设为下图的上轴QCustomPlot 会把图元画到以这两条轴交叉定义的坐标系里形状可能很怪但不会报错。这种灵活性是双刃剑排查问题时首先要检查的就是每个 graph 的轴归属。2.3 先跑一个最小示例同一面板挂两条不同纵轴的曲线先把最常见的场景跑通同一个 xAxis左边一条 Y 轴显示温度右边一条 Y 轴显示压力。代码如下QCustomPlot *plot new QCustomPlot(this); QVectordouble x {1, 2, 3, 4, 5}; QVectordouble temperature {36.2, 36.5, 36.8, 37.1, 36.9}; QVectordouble pressure {1024, 2048, 3072, 4096, 5120}; // 第一条曲线使用默认的左Y轴 QCPGraph *tempGraph plot-addGraph(); tempGraph-setData(x, temperature); tempGraph-setPen(QPen(QColor(#d32f2f))); tempGraph-setName(温度); // 启用右侧Y轴作为第二条值轴 plot-axisRect()-addAxis(QCPAxis::atRight); QCPAxis *rightAxis plot-axisRect()-axis(QCPAxis::atRight); // 第二条曲线横轴仍旧是默认下轴值轴换成右侧轴 QCPGraph *pressureGraph plot-addGraph(); pressureGraph-setKeyAxis(plot-xAxis); pressureGraph-setValueAxis(rightAxis); pressureGraph-setData(x, pressure); pressureGraph-setPen(QPen(QColor(#1976d2))); pressureGraph-setName(压力); plot-legend-setVisible(true); plot-rescaleAxes(); plot-replot();这段代码的关键是第二处 addGraph 之后的 setKeyAxis 和 setValueAxis。如果不做这两步pressureGraph 会与 tempGraph 共用左轴压力数据会瞬间把温度曲线压成一条平线。addAxis(QCPAxis::atRight)的作用是在默认 axisRect 上创建或激活右侧轴axis(QCPAxis::atRight) 拿到的是该轴的指针后续设置范围、标签、颜色都靠它。rescaleAxes()会遍历当前 plotLayout 中所有可见坐标轴并统一调整范围让所有曲线都完整显示这在初次跑通时很有用但后续做精细布局时往往要换成手动 setRange。3. 同一个面板里建多坐标系共享 X 轴多 Y 轴的写法与参数3.1 右侧加一条值轴前先想好三件事显示、标签、范围用addAxis(QCPAxis::atRight)把右侧轴拉出来后右侧轴默认是不显示的很多人在这里翻车代码写了轴也加了但界面上没有任何变化。原因很简单QCustomPlot 的默认轴xAxis、yAxis在创建时就设为可见而右侧轴对象存在但 visible 属性默认为 false。你需要主动设置QCPAxis *rightAxis plot-axisRect()-axis(QCPAxis::atRight); rightAxis-setVisible(true); rightAxis-setLabel(压力Pa); rightAxis-setTickLabelColor(QColor(#1976d2)); rightAxis-setLabelColor(QColor(#1976d2));这段代码里 setVisible(true) 是最容易漏的一行。右侧轴显示出来后建议同时设置轴标签和刻度颜色并且与曲线颜色保持一致。这样看图的人扫一眼就知道蓝色刻度对应蓝色曲线红色刻度对应温度曲线不需要额外做图例说明。范围也要提前想好。如果数据是实时采集的压力可能从 1000 跳到 6000轴范围如果写死曲线很容易冲出视野。我一般分两种情况处理固定量程的仪表类场景用 setRange(min, max) 写死采集类场景在数据更新时调用 pressureGraph-rescaleValueAxis(true)让纵轴跟随当前数据自动缩放横轴则保持时间窗口滚动。3.2 刻度、网格与曲线颜色三个地方必须同步多 Y 轴共用一个绘图区时最容易出现的视觉混乱是左右两条 Y 轴的刻度都显示在面板里网格线叠在一起分不清哪条刻度线属于哪条曲线。默认情况下右侧轴一旦可见也会像左轴一样绘制自己的网格线两条轴的刻度值大小完全不同网格在图上会形成两层交错的水平线非常干扰读图。解决方案是把右侧轴的网格关掉只保留左轴的网格。网格是绘图区背景上的东西不是轴本身的数据所以关掉右侧轴的 grid 不会影响任何曲线rightAxis-grid()-setVisible(false);如果你希望右侧轴也有网格只是换个颜色区分也可以设置 grid 的 pen。但根据我的经验同一绘图区里网格画两套基本没有正面效果关掉右轴网格是常态做法。刻度颜色、轴标签颜色、曲线颜色三处要保持一致这个习惯能在后期省很多事。颜色同步这件事没有 API 能一键完成需要在代码里维护一个「曲线参数表」之类的结构体每次新增曲线时把 pen、tickLabelColor、labelColor 一起设置。不要让颜色散落在各个 setter 调用里后面维护起来会非常痛苦。3.3 对数轴与线性轴并存坐标类型切换的几个关键设置多坐标系里还有一种常见需求同一面板的右轴用对数刻度左轴用线性刻度。温度变化是线性的压力可能跨两三个数量级线性轴画出来小数值段完全看不见。QCustomPlot 切换到对数坐标比较简单但有几个边界条件必须处理rightAxis-setScaleType(QCPAxis::stLogarithmic); QSharedPointerQCPAxisTickerLog logTicker(new QCPAxisTickerLog); logTicker-setLogBase(10.0); logTicker-setSubTickCount(8); rightAxis-setTicker(logTicker);第一setScaleType 必须在创建 ticker 之前或之后都可以但要保证 ticker 类型与 scaleType 匹配。用线性 ticker 配对数轴刻度位置会乱。第二对数轴的范围下限必须是正数也就是说 setRange(0, 10000) 在这种轴上会失效因为 0 的对数没有定义。第三subTickCount 控制每个主刻度之间的细分刻度数量对数轴的子刻度不是等距的数量设置要看你的数据跨度跨度大就多设几个。对数轴上线后rescaleAxes 的行为也要留意。QCustomPlot 的自动缩放会对对数轴取对数后计算范围如果你的数据里有 0 或负值rescaleAxes 会得到非法范围整条曲线可能直接不显示。实时数据场景下我通常会在数据更新的回调里判断最小值是否大于 0再做 rescale。4. 多个面板才是真多坐标系分栏 axisRect 的布局与独立缩放4.1 上下叠两个坐标系addAxisRect 与 plotLayout 行分配当横轴含义不同、上下两幅图需要独立滚动时就要往 plotLayout 里加新的 QCPAxisRect。默认的 plotLayout 是一行一列加入第二个 axisRect 后需要把它放到第二行。写法如下QCPAxisRect *mainRect plot-axisRect(); QCPAxisRect *subRect new QCPAxisRect(plot); plot-plotLayout()-addElement(1, 0, subRect); plot-plotLayout()-setRowStretchFactor(0, 3); plot-plotLayout()-setRowStretchFactor(1, 1);关键在于 setRowStretchFactor。如果不设两行会等高主坐标系和子坐标系平分上下空间。如果上方是主波形下方是辅助频谱把比例设为 3:1 更符合阅读习惯。addElement(1, 0) 的意思是放入第一行第零列行号从 0 开始0 行已经放着默认 axisRect所以新增的一定从 1 开始。subRect 在创建时默认也带四条轴和主 rect 一样只有左右下三条可见。对这个坐标系里的曲线做绑定基本逻辑和第 2 章的 graph 绑定完全一致只是指向的轴要换成 subRect 自己的QCPAxis *subX subRect-axis(QCPAxis::atBottom); QCPAxis *subY subRect-axis(QCPAxis::atLeft); QCPGraph *fftGraph plot-addGraph(); fftGraph-setKeyAxis(subX); fftGraph-setValueAxis(subY); fftGraph-setPen(QPen(QColor(#388e3c)));从这段代码能看到addGraph 仍然作用在 plot 上所以 QCustomPlot 的图元列表是全局的坐标系只是图元的绘制宿主。addGraph 的次数不受 axisRect 数量限制同一时刻你可以在一个 graph 里绑定任意 rect 的任意轴。4.2 每个坐标系独立缩放setRange 与 rescaleAxes 的作用域多个 axisRect 并存后「独立」两个字是核心诉求。上下两幅图的数据量级和关注点不同缩放时必须各管各的。这里有三个层级的作用域一定要分清楚plot-rescaleAxes() 作用于整个 plot 内所有可见轴不分 rect。这条命令在多坐标系下基本不要用因为你大概率只希望下幅图重新适应数据上幅图保持用户手动缩放的位置。graph-rescaleValueAxis(true) 只作用于该 graph 绑定的值轴横轴不动。最精确的是直接对轴调用 setRange。鼠标交互层面QCustomPlot 的默认行为是鼠标在哪个 axisRect 上拖动就缩放哪个 rect 的轴。这个行为在单 rect 时感觉不明显多 rect 之后才会意识到它设计得挺合理。但要注意如果你设置了 axisRect-setRangeDragAxes 或 setRangeZoomAxes把两个 rect 的轴混在了一起交互就会串。这些 setter 接收的是 QCPAxis 指针不是 rect 指针所以完全可以把主坐标系的 X 轴和子坐标系的 X 轴绑到同一个拖拽行为上实现两个坐标系横向同步滚动。4.3 坐标系之间的联动与取消联动margin 处理与拖拽冲突有时我们希望两个坐标系的上限对齐或者纵轴范围同步这需要显式连接轴信号。QCPAxis 有一个 rangeChanged 信号在用户拖动或缩放该轴时触发可以把它转发给另一个坐标系的轴connect(mainRect-axis(QCPAxis::atBottom), QCPAxis::rangeChanged, this, [subRect](const QCPRange range) { subRect-axis(QCPAxis::atBottom)-setRange(range); });这种联动的代价是一旦连接建立两个坐标系的下轴会互相锁定用户在下幅图横向拖动时上幅图也会跟着动。这不是 bug是联动本身的设计。如果你想做「单向联动」比如只允许主坐标系带动子坐标系子坐标系反过来不能带动主坐标系那就要在 rangeChanged 信号里加一个标志位或者把子坐标轴的交互关掉。还有一个 margin 的坑新增 axisRect 之后默认不会给第二个轴留出足够的左边距子坐标系的 Y 轴标签可能会被裁剪。遇到这种情况需要手动调整 rect 的 marginssubRect-setAutoMargins(QCP::msLeft | QCP::msRight); subRect-setMargins(QMargins(55, 0, 5, 0));我一般用 setAutoMargins 保留左右方向的自动计算再额外给左边加固定边距确保多位数的轴标签不会跟图形边缘重叠。5. 多坐标系避坑我每次都会踩的 5 个坑与现场处理5.1 曲线显示在另一个坐标系里axisRect 归属没设现象明明新建了 subRect也把 graph 的 valueAxis 指向了 subRect 的左轴但曲线还是画在主坐标系上。原因graph 的 keyAxis 没有设置默认仍指向主坐标系的下轴。keyAxis 和 valueAxis 必须成对设置只改 valueAxis 会让图元的两条轴分别属于两个坐标系列绘制位置会落在以这两条轴为边界的坐标系里外观上经常表现为曲线出现在主 rect 的奇怪位置。解决绑定 graph 时写完 setValueAxis 紧接着写 setKeyAxis。我习惯把绑定写成两行连着的代码并注释「轴必须显式指定默认值只对第一个 rect 有意义」。5.2 缩放一个坐标系导致另一个也跟着动range 拖拽的交叉响应现象在子坐标系上滚轮缩放主坐标系的横轴也跟着缩放两个 rect 的画面同步跳跃。原因最常见的是你不小心用了 plot-xAxis 作为子坐标系的 keyAxis主坐标系的下轴和子坐标系的下轴是同一个指针缩放当然联动。另一个原因是 rangeChanged 联动连接过多形成了双向绑定。解决第一步检查每个 graph 的 keyAxis 是否真的属于 subRect用 qDebug 打印 axis 指针地址主轴的地址和子轴地址必须不同。第二步检查 connect 的联动是否只建立了单向。多坐标系里指针复用是最隐蔽的坑因为代码编译能过、运行时也不报错只有行为异常。5.3 两套刻度线与网格错位tick 与 subTick 没调现象主轴的网格线密度正常右侧轴或子坐标系的网格线特别疏或特别密与曲线数据看上去对不上。原因每个轴有自己独立的 ticker子坐标系的 ticker 不会自动跟随主轴的步长。量级相差大的数据默认 ticker 生成的步长可能相差十倍以上网格线当然对不上。解决为每个轴显式创建 QCPAxisTicker 并设置 tickStep。如果子坐标系只是辅助参考直接把子坐标系的网格关掉只保留主轴网格视觉上更干净。网格只是背景参考不允许它在多坐标系下喧宾夺主。5.4 隐藏坐标系后图例还占着位置legend 与 axisRect 的绑定现象把子坐标系通过 setVisible(false) 隐藏后主坐标系下方多出一块空白或者图例跑到看不到的位置。原因legend 默认只对应主 axisRect。QCustomPlot 的 legend 本质上是一个 QCPLayoutGrid 里的条目容器它显示在哪个 rect 区域由所属 layout 决定。隐藏 rect 时如果 legend 仍然挂在这个 rect 里它不会跟着隐藏。解决隐藏子坐标系前先把 legend 切到主 rect 上。更稳妥的做法是多坐标系场景下干脆使用继承的 QCPLegend 对象并显式设置 legend 所属的 axisRect 后再统一控制可见性。涉及多个 rect 的布局操作不要依赖默认归属所有属性都显式指定。5.5 析构时崩溃或悬挂指针axis 的生命周期归属现象程序退出时崩溃或者 removeGraph 后再次访问 axis 指针报野指针错误。原因QCustomPlot 对轴和图元的管理是「谁创建谁拥有」。axisRect 属于 plotLayout轴属于 axisRectgraph 属于 QCustomPlot 的 plottable 列表。直接 delete 一个轴但 graph 的 valueAxis 还指向它replot 时就会访问已释放的内存。解决删除轴的顺序必须先解除 graph 对它的引用。常见安全顺序是先 plot-removeGraph(graph)再 delete axis。删除 axis 用 plot-axisRect()-removeAxis 或直接 delete取决于 QCustomPlot 版本。最保险的析构习惯是只 delete 顶层的 QCustomPlot 对象让所有子对象按归属链自动析构不要手动逐个 delete 轴和 rect。6. 坐标系统一缩放的技巧把主坐标系的 range 传给其余坐标系多坐标系做多了之后会发现真正高频的需求是主坐标系横向滚动时子坐标系跟着滚动主坐标系手动缩放时子坐标系同步缩放。第 4 章提过可以用 rangeChanged 信号转发但实际上有一个更稳的写法能避免信号递归。代码如下bool syncingRanges false; connect(mainRect-axis(QCPAxis::atBottom), QCPAxis::rangeChanged, this, [this, subRect, subX](const QCPRange range) { if (syncingRanges) return; syncingRanges true; subX-setRange(range); syncingRanges false; }); connect(subX, QCPAxis::rangeChanged, this, [this, mainRect](const QCPRange range) { if (syncingRanges) return; syncingRanges true; mainRect-axis(QCPAxis::atBottom)-setRange(range); syncingRanges false; });亮点是 syncingRanges 标志位。两个轴互发 rangeChanged 时如果不加标志位A setRange 触发 B 的信号B setRange 再触发 A 的信号会形成无限递归。加了标志位后信号回调里主动 setRange 引起的后续信号会被忽略数据不会死循环。这个技巧解决了多坐标系里最烦人的信号风暴问题。但要注意QCPAxis 的 rangeChanged 是在轴范围实际变化时发出的如果两个坐标系的数据长度和比例不同同步后子坐标系可能显示得很空。所以我一般只同步横向范围纵向范围各坐标系独立自动缩放——模拟量波形和频谱分析这两种场景纵轴同步反而会让辅助图失去意义。关于 QCustomPlot 版本差异我也想提一句2.x 之后信号机制的细节和 1.x 有些不一样rangeChanged 的递归行为在 1.x 上更严重务必在目标版本上实测一次信号风暴。不要在理论上觉得自己想清楚了就跳过验证这是我在这上面交过学费的地方。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。