跳转到内容
新建笔记

QCustomPlot 坐标轴刻度:读取绘图结果与 ticker 生成规则

QCustomPlot 坐标轴的主刻度由安装在轴上的 ticker 生成。要得到与绘图一致的刻度,需要区分“读取上次绘图生成的刻度”和“按当前配置重新计算刻度”;从 range().lower 开始反复加步长,会忽略刻度原点与缩放策略。

本文按 QCustomPlot 2.1.1、Qt 6、C++17 编写,完整程序已在 Qt 6.10.2 上编译运行。

原需求写成一个返回 QList<double> 的 Lambda。[] 是空捕获,QCPAxis *axis 是指针参数,-> QList<double> 是显式返回类型,auto getTicks = … 保存闭包对象,再用 getTicks(axis) 调用。语法本身见 Lambda 与函数对象。

问题在函数体:

  • static_cast<QCPAxisTickerFixed *>(axis->ticker().data()) 不检查 ticker 的实际类型。结果非空不能证明它是 Fixed;把其他 ticker 当作 Fixed 使用会产生未定义行为。
  • 刻度通常落在 origin + k * step,不一定从轴范围下限开始。例如范围 [0.3, 3.7]、原点 0.5、步长 1,主刻度是 0.5, 1.5, 2.5, 3.5。
  • ssMultiples 或 ssPowers 允许实际步长改变,tickStep() 返回的配置值不必等于当前绘制间隔。日期、文字、对数 ticker 更不能一律套用等间距算法。
  • 空轴指针、非有限数、过密刻度和浮点加法无法继续前进都需要考虑;给上界简单加 1e-6 不能解决这些问题。2.1.1 的 setTickStep() 会拒绝非正步长,但调用方仍应检查来自输入的数值与刻度数量。

完整例子:实际刻度与主动生成

跳转到“完整例子:实际刻度与主动生成”

把官方发行包中的 qcustomplot.h、qcustomplot.cpp 放到项目中,链接 Widgets 和 PrintSupport,并对 QCustomPlot 启用 AUTOMOC。以下代码保存为 main.cpp。

#include <QApplication>
#include <QLocale>
#include <QSharedPointer>
#include <QVector>
#include <cassert>
#include "qcustomplot.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QCustomPlot plot;
plot.resize(640, 360);
auto fixed = QSharedPointer<QCPAxisTickerFixed>::create();
fixed->setTickStep(1.0);
fixed->setTickOrigin(0.5);
fixed->setScaleStrategy(QCPAxisTickerFixed::ssNone);
plot.xAxis->setTicker(fixed);
plot.xAxis->setRange(0.3, 3.7);
plot.replot(QCustomPlot::rpImmediateRefresh);
// 读取最近一次完成布局、绘图准备时生成的主刻度。
const auto readTicks = [](const QCPAxis *axis) -> QVector<double> {
return axis ? axis->tickVector() : QVector<double>{};
};
const QVector<double> expected{0.5, 1.5, 2.5, 3.5};
assert(readTicks(plot.xAxis) == expected);
assert(readTicks(nullptr).isEmpty());
// 主动按当前范围与 ticker 配置生成,无需向下转换。
QVector<double> generated;
plot.xAxis->ticker()->generate(plot.xAxis->range(), QLocale::c(),
'g', 6, generated, nullptr, nullptr);
assert(generated == expected);
plot.xAxis->setRange(-1.7, 1.7);
plot.replot(QCustomPlot::rpImmediateRefresh);
assert((readTicks(plot.xAxis) == QVector<double>{-1.5, -0.5, 0.5, 1.5}));
fixed->setScaleStrategy(QCPAxisTickerFixed::ssMultiples);
plot.xAxis->setRange(0.0, 100.0);
plot.replot(QCustomPlot::rpImmediateRefresh);
const auto scaled = readTicks(plot.xAxis);
assert(scaled.size() >= 2);
assert(scaled[1] - scaled[0] > fixed->tickStep());
// 普通 ticker 也能读取,无需冒险转换成 Fixed。
plot.xAxis->setTicker(QSharedPointer<QCPAxisTicker>::create());
plot.replot(QCustomPlot::rpImmediateRefresh);
assert(!readTicks(plot.xAxis).isEmpty());
}

对应的最小 CMake 配置如下。MinGW 构建 2.1.1 的大源文件时可能需要 -Wa,-mbig-obj,否则汇编器会报 sections 过多;这是构建选项,不需要修改库源码。

cmake_minimum_required(VERSION 3.20)
project(AxisTicks LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_AUTOMOC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets PrintSupport)
add_executable(axis_ticks main.cpp qcustomplot.cpp qcustomplot.h)
target_link_libraries(axis_ticks PRIVATE Qt6::Widgets Qt6::PrintSupport)
if(MINGW)
target_compile_options(axis_ticks PRIVATE -Wa,-mbig-obj)
endif()

tickVector() 返回轴内部保存的主刻度。改变范围后,如果只请求排队重绘,立即读取可能仍是旧数据;可以在 afterReplot 通知后读取。例子用立即重绘建立清楚的读取时点。轴的刻度、标签和网格全部关闭时,内部可能跳过刻度更新,因此缓存不应被当作任何配置下都会重新计算的接口。

generate() 通过基类的虚函数调用实际 ticker 的规则,包含原点、缩放与可见范围裁剪;示例只要求数值位置,所以采用 C locale、'g' 和精度 6。需要与轴完全一致的标签时,同时使用绘图 locale、轴的数字格式及精度,并提供标签输出参数。副刻度要另外提供 subTicks 输出参数,它们不在 tickVector() 中。

需要固定步长时,像例子一样创建并保存正确类型的智能指针。如果必须识别一个来源未知的 ticker,可用有运行时类型检查的转换并检查失败,但“读取主刻度”本身不需要转换。以上所有 QWidget、轴和绘图访问均在 GUI 线程完成。

若范围、原点和步长来自输入,进入绘图库前应确认数值有限、下限小于上限、步长严格为正,并按范围与步长估算主刻度数量,设置合理上限。固定步长在极宽范围内可能生成海量刻度;可选择 ssMultiples 或降低显示密度。对数轴则有自己的范围与 ticker 契约。

若需求只是生成业务用的等距采样点,应单独定义起点、终点、端点是否包含及最大数量,使用有界的整数索引计算;不要把这个结果称为“当前坐标轴刻度”。

实现依据为 QCustomPlot 2.1.1 官方源码包 中的 QCPAxisTicker::generate()、createTickVector()、QCPAxisTickerFixed::getTickStep() 与 QCPAxis::setupTickVectors();接口说明见 QCPAxisTicker 和 QCPAxisTickerFixed。