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。