当前位置: 首页 > article >正文

QCustomPlot 深度解析:从渲染架构到源码内幕

一、QCustomPlot 是什么不是什么QCustomPlot 是一个 Qt 绘图库核心就两个文件qcustomplot.hqcustomplot.cpp。不是 Qt 官方库不属于 Qt 模块但做得比 Qt Charts 干净得多。设计哲学扩展 Qt 的 QPainter而不是发明一套新渲染引擎。这意味着QCustomPlot 画图的底层工具就是 Qt 的QPainter所有图表元素的绘制都在paint()方法里用painter-drawLine()、painter-drawText()、painter-drawRect()完成。理解这一点源码就容易读懂。支持的主要图表类型类图表类型QCPGraph折线图、柱状图QCPCurve参数曲线QCPFinancialK 线图、OHLCQCPBars柱状图QCPColorMap热力图QCPStatisticalBox箱线图QCPErrorBars误差棒QCPBanner极坐标雷达图二、类层次结构全览QObject └── QCustomPlot ← 主控件持有所有元素 ├── QCPLayoutGrid ← 网格布局多图排列 ├── QCPAxisRect ← 坐标轴矩形可多个 │ ├── QCPAxis [xBottom] ← X 轴 │ ├── QCPAxis [yLeft] ← Y 轴 │ ├── QCPAxis [xTop] │ └── QCPAxis [yRight] │ └── QListQCPAbstractPlottable* ← 绑定到此轴矩形的图表 │ ├── QCPAbstractPlottable ← 图表基类 │ ├── QCPGraph ← 折线/柱状 │ ├── QCPCurve ← 参数曲线 │ ├── QCPFinancial ← K 线/OHLC │ ├── QCPBars ← 柱状图 │ ├── QCPColorMap ← 热力图 │ └── QCPStatisticalBox ← 箱线图 │ └── QCPAbstractItem ← 装饰层基类不绑数据纯绘图 ├── QCPItemLine ← 直线 ├── QCPItemRect ← 矩形 ├── QCPItemText ← 文本 ├── QCPItemCurve ← 曲线 ├── QCPItemBracket ← 大括号 ├── QCPItemPixmap ← 图片 └── QCPItemTrace ← 跟踪线 QCPAxisTicker (轴刻度生成器) ├── QCPAxisTickerFixed ← 固定间隔 ├── QCPAxisTickerLog ← 对数 ├── QCPAxisTickerDateTime ← 日期时间最重要 ├── QCPAxisTickerPi ← π 倍数 └── QCPAxisTickerText ← 文本标签三、QCustomPlot 主类源码解析3.1 绘图入口QCustomPlot::paintEvent// qcustomplot.cpp void QCustomPlot::paintEvent(QPaintEvent *event) { Q_UNUSED(event) QCPPainter painter(this); // QCustomPlot 封装的 QPainter painter.setMode(QCPPainter::pmAutoPaint); // 自动混合 // 核心按层级依次绘制 // Layer 1: Background painter.fillRect(rect(), brush()); // Layer 2: Layout (axes grid) if (m_plotLayout) m_plotLayout-paint(painter); // Layer 3: Plottables (graphs, curves, financial...) for (int i 0; i plottableCount(); i) { plottable(i)-draw(painter); } // Layer 4: Items (lines, text, arrows...) for (int i 0; i itemCount(); i) { item(i)-draw(painter); } // Layer 5: Selection boxes, zoom rect if (m_selectionRect m_selectionRect-active()) m_selectionRect-draw(painter); }三层渲染架构Layout 层— 坐标轴矩形、网格、刻度标签Plottable 层— 图表数据K 线、折线、热力图Item 层— 装饰性元素十字线、文本、箭头3.2 QCPPainter扩展 QPainter// qcustomplot.h class QCustomPlot::QCPPainter : public QPainter { public: enum PainterMode { pmDefault, pmAutoPaint, pmNoCaching, pmVectorized }; explicit QCPPainter(QPaintDevice *device); void setMode(PainterMode mode); // QCustomPlot 特有的接口 void setPen(const QPen pen) override; void setBrush(const QBrush brush) override; // 抗锯齿文本绘制QPainter::drawText 抗锯齿效果差 void drawText(const QPointF pos, const QString text); private: PainterMode m_mode; bool m Modes[pmPaintModes]; // pmPensChanged, pmBrushesChanged, ... };QCPPainter 的核心价值QPainter 原生的drawText抗锯齿差QCPPainter重写了这个方法用QStaticText缓存实现了高质量文本渲染同时处理了缩放时的亚像素渲染。四、QCPAbstractPlottable所有图表的基类4.1 抽象接口// qcustomplot.h (核心部分) class QCP_LIB_DECL QCPAbstractPlottable : public QCPLayerable { Q_OBJECT public: explicit QCPAbstractPlottable(QCPAxis *keyAxis, QCPAxis *valueAxis); virtual ~QCPAbstractPlottable(); // 必须重写的虚函数 virtual void draw(QCPPainter *painter) 0; virtual QCPRange getKeyRange(bool foundRange, QCP::SignDomain signDomain QCP::sdBoth) const 0; virtual QCPRange getValueRange(bool foundRange, QCP::SignDomain signDomain QCP::sdBoth, const QCPRange inKeyRange QCPRange(-1, 1)) 0; // 数据管理 virtual double selectTest(const QPointF pos, bool details, QVariant *details nullptr) const; virtual QCPDataSelection selections() const; virtual void clearData(); // 绑定坐标轴 QCPAxis *keyAxis() const { return mKeyAxis; } QCPAxis *valueAxis() const { return mValueAxis; } void setKeyAxis(QCPAxis *axis); void setValueAxis(QCPAxis *axis); // 可见性 bool visible() const { return mVisible; } void setVisible(bool on) { mVisible on; } protected: QCPAxis *mKeyAxis; QCPAxis *mValueAxis; bool mVisible; // 选中管理 QCPDataSelection mSelection; QCPDataSelection mSelectTestCache; };4.2 数据容器QCPDataMap// qcustomplot.h // QCPGraph 的数据结构key (x) → data 结构体 typedef QMapdouble, QCPGraphData QCPDataMap; struct QCPGraphData { double key; // X 轴值通常是时间戳或索引 double value; // Y 轴值 }; struct QCPFinancialData { double key; // X 轴时间 double open; double high; double low; double close; }; struct QCPCurveData { double t; // 参数 t double key; // X double value; // Y }; struct QCPBarData { double key; double value; }; // 源码里的 key 查找BST double QCPDataMap::findBegin(double key, bool expandedRange) const { // QMap 是红黑树lowerBound 是 BST 查找 // 这是 QCustomPlot 大数据量时的性能瓶颈之一 // 改进方案用 QCPDataContainer 替代 iterator it lowerBound(key - (expandedRange ? 1e-10 : 0)); return it.key(); }五、QCPGraph折线图源码5.1 draw() 方法// qcustomplot.cpp void QCPGraph::draw(QCPPainter *painter) { // 1. 获取可见数据段 QCPDataContainerQCPGraphData::iterator itBegin, itEnd; getVisibleData(itBegin, itEnd); if (itBegin itEnd) return; // 2. 应用画笔 painter-setPen(mPen); painter-setBrush(Qt::NoBrush); // 3. 根据图表类型选择渲染方式 if (mLineStyle ! lsNone) { // 画线转换为屏幕坐标逐段 drawLine QVectorQPointF lines; lines.reserve(std::distance(itBegin, itEnd)); QCPRange rangeX mKeyAxis-range(); QCPRange rangeY mValueAxis-range(); for (auto it itBegin; it ! itEnd; it) { // 数据坐标 → 像素坐标轴负责转换 double px mKeyAxis-coordToPixel(it-key); double py mValueAxis-coordToPixel(it-value); lines.append(QPointF(px, py)); } // 一次性画多条线比逐条 drawLine 快 painter-drawLines(lines); } // 4. 画散点 if (mScatterStyle ! QCPScatterStyle::ssNone) { painter-setPen(mScatterPen); painter-setBrush(mScatterBrush); for (auto it itBegin; it ! itEnd; it) { double px mKeyAxis-coordToPixel(it-key); double py mValueAxis-coordToPixel(it-value); mScatterStyle.applyTo(painter, QPointF(px, py)); } } }5.2 坐标转换// qcustomplot.cpp double QCPAxis::coordToPixel(double coord) const { if (!mScaleType) // 线性 return mAxisRect-left() (coord - mRange.lower) / (mRange.upper - mRange.lower) * mAxisRect-width(); else // 对数 return mAxisRect-left() qLn(coord / mRange.lower) / qLn(mRange.upper / mRange.lower) * mAxisRect-width(); } double QCPAxis::pixelToCoord(double pixel) const { if (!mScaleType) // 线性 return mRange.lower (pixel - mAxisRect-left()) / mAxisRect-width() * (mRange.upper - mRange.lower); else // 对数 return mRange.lower * qPow(mRange.upper / mRange.lower, (pixel - mAxisRect-left()) / mAxisRect-width()); }六、QCPFinancialK 线图源码6.1 核心数据结构// qcustomplot.h class QCP_LIB_DECL QCPFinancial : public QCPAbstractPlottable { public: // K 线两种样式 enum ChartStyle { csCandlestick, csOHLC }; // OHLC 数据开盘/最高/最低/收盘 struct OHLC { double key, open, high, low, close; }; // 内部用 QVector 存储连续内存比 QMap 快 // QVectorQPointF mData; // key, open/high/low/close (交错存储) };6.2 draw() 方法// qcustomplot.cpp void QCPFinancial::draw(QCPPainter *painter) { QCPDataContainerQCPFinancialData::iterator itBegin, itEnd; getVisibleData(itBegin, itEnd); if (itBegin itEnd) return; const double halfWidth mWidth法师 * 0.5; // K 线半宽度 for (auto it itBegin; it ! itEnd; it) { const QCPFinancialData dp *it; // 转换到屏幕坐标 double keyPixel mKeyAxis-coordToPixel(dp.key); double openPixel mValueAxis-coordToPixel(dp.open); double highPixel mValueAxis-coordToPixel(dp.high); double lowPixel mValueAxis-coordToPixel(dp.low); double closePixel mValueAxis-coordToPixel(dp.close); // K 线颜色涨收盘 开盘/ 跌收盘 开盘 bool isUp dp.close dp.open; QColor fillColor isUp ? mBrushPositive.color() : mBrushNegative.color(); QPen linePen isUp ? mPenPositive : mPenNegative; painter-setPen(linePen); painter-setBrush(fillColor); if (mChartStyle csCandlestick) { // 上下影线 painter-drawLine(QPointF(keyPixel, highPixel), QPointF(keyPixel, lowPixel)); // 实体矩形 painter-drawRect(QRectF( keyPixel - halfWidth, qMin(openPixel, closePixel), halfWidth * 2, qAbs(closePixel - openPixel) )); } else { // OHLC四条横线 painter-drawLine(QPointF(keyPixel - halfWidth, openPixel), QPointF(keyPixel halfWidth, openPixel)); painter-drawLine(QPointF(keyPixel - halfWidth, closePixel), QPointF(keyPixel halfWidth, closePixel)); painter-drawLine(QPointF(keyPixel, highPixel), QPointF(keyPixel, lowPixel)); } } }七、QCPAxisTickerDateTime时间轴源码时间轴是 QCustomPlot 最常用的功能也是源码里最复杂的部分之一。7.1 日期时间刻度生成策略// qcustomplot.cpp void QCPAxisTickerDateTime::calcTickStep(const QCPRange range, const QVariant divisionHint) { double elapsed range.upper - range.lower; // 时间跨度秒 // 根据跨度选择刻度密度 if (elapsed 60) { // 1 分钟秒级刻度 mTickStep 1; mSmallestFormat hh:mm:ss; mDateFormat hh:mm:ss; } else if (elapsed 3600) { // 1 小时分钟级 mTickStep 60; mSmallestFormat hh:mm; mDateFormat hh:mm; } else if (elapsed 86400 * 3) { // 3 天小时级 mTickStep 3600; mSmallestFormat hh:mm; mDateFormat ddd hh:mm; } else if (elapsed 86400 * 30) { // 1 个月天级 mTickStep 86400; mSmallestFormat dd; mDateFormat MMM dd; } else if (elapsed 86400 * 365) { // 1 年月级 mTickStep 86400 * 30; mSmallestFormat MMM; mDateFormat MMM yyyy; } else { // 1 年年级 mTickStep 86400 * 365; mSmallestFormat yyyy; mDateFormat yyyy; } } double QCPAxisTickerDateTime::dateTimeToKey(const QDateTime dt) const { // QDateTime → doubleUnix 时间戳秒为单位 // Qt 6 推荐写法避免废弃警告 return double(QDateTime(dt).toSecsSinceEpoch()); } QDateTime QCPAxisTickerDateTime::keyToDateTime(double key) const { return QDateTime::fromSecsSinceEpoch(qint64(key), Qt::UTC); }7.2 刻度标签格式化// qcustomplot.cpp QString QCPAxisTickerDateTime::getTickLabel(const QCPAxisTick tick) const { QDateTime dateTime keyToDateTime(tick.position); // 根据跨度选择显示格式 double elapsed mAxis-range().upper - mAxis-range().lower; if (elapsed 86400) { // 1 天显示时间部分 return dateTime.toString(hh:mm:ss); } else if (elapsed 86400 * 30) { // 1 个月显示日期 时间 return dateTime.toString(yyyy-MM-dd\nhh:mm); } else if (elapsed 86400 * 365) { // 1 年显示月日 return dateTime.toString(yyyy-MM-dd); } else { // 1 年显示年月 return dateTime.toString(yyyy-MM); } }八、QCPLayoutSystem多图布局8.1 网格布局// qcustomplot.cpp void QCPLayoutGrid::paint(QCPPainter *painter) { // 遍历所有格子 for (int row 0; row rowCount(); row) { for (int col 0; col columnCount(); col) { QCPLayoutElement *elem element(row, col); if (!elem) continue; // 计算当前格子的实际矩形 QRectF rect QRectF( mColumnSpacingFactors[col].offset * totalWidth innerRect.left(), mRowSpacingFactors[row].offset * totalHeight innerRect.top(), mColumnSpacingFactors[col].size * totalWidth, mRowSpacingFactors[row].size * totalHeight ); // 设置布局矩形并绘制元素 elem-setOuterRect(rect); elem-paint(painter); } } }九、交互系统缩放与拖拽9.1 QCP::Interactions 标志位// qcustomplot.h namespace QCP { enum Interaction { iRangeDrag 0x001, // 拖拽 iRangeZoom 0x002, // 缩放 iMultiSelect 0x004, // 多选 iSelectPlottables 0x008, // 选中图表 iSelectAxes 0x010, // 选中坐标轴 iSelectLegend 0x020, // 选中图例 iSelectItems 0x040, // 选中装饰项 iSelectOther 0x080, // 其他 }; Q_DECLARE_FLAGS(Interactions, Interaction) } // 使用 customPlot-setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); customPlot-axisRect()-setRangeDrag(Qt::Horizontal); // 只横向拖拽 customPlot-axisRect()-setRangeZoom(Qt::Horizontal); // 只横向缩放9.2 鼠标事件处理链// qcustomplot.cpp void QCustomPlot::mousePressEvent(QMouseEvent *event) { if (mInteractions.testFlag(QCP::iRangeDrag) mDragging 0) { startRangeDrag(event-pos()); mDragging 1; } // 选中测试 if (event-modifiers() Qt::ControlModifier) { selectTest(event-pos(), QCP::stQCP); update(); } } void QCustomPlot::wheelEvent(QWheelEvent *event) { if (!mInteractions.testFlag(QCP::iRangeZoom)) return; // 滚轮缩放根据鼠标位置确定缩放中心 if (axisRect()-rect().contains(event-position().toPoint())) { double key m_xAxis-pixelToCoord(event-position().x()); double factor event-angleDelta().y() 0 ? 0.9 : 1.1; // 缩放系数 QCPRange range m_xAxis-range(); range.scale(factor, QCPRange::sdWithin); m_xAxis-setRange(range); update(); // 重绘 } }十、实战代码示例10.1 基础折线图// mainwindow.cpp void MainWindow::setupLineChart() { // 创建 QCustomPlot QCustomPlot *plot new QCustomPlot(this); plot-setGeometry(50, 50, 800, 500); plot-setBackground(Qt::white); // 添加一条折线 QCPGraph *graph plot-addGraph(); graph-setName(Data Series); // 生成 1000 个数据点 QVectordouble x(1000), y(1000); for (int i 0; i 1000; i) { x[i] i; y[i] qSin(i / 50.0) qrand() / double(RAND_MAX) * 0.5; } graph-setData(x, y); // 配置 X 轴 plot-xAxis-setLabel(Time (s)); plot-xAxis-setRange(0, 1000); // 配置 Y 轴 plot-yAxis-setLabel(Amplitude); plot-yAxis-setRange(-2, 2); // 开启拖拽和缩放 plot-setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); // 添加十字光标 QCPItemTracer *tracer new QCPItemTracer(plot); tracer-setGraph(graph); tracer-setInterpolating(true); tracer-setStyle(QCPItemTracer::tsCrosshair); tracer-setPen(QPen(Qt::gray)); tracer-setBrush(Qt::NoBrush); // 鼠标移动时更新光标位置 connect(plot, QCustomPlot::mouseMove, this, [](QMouseEvent *event) { tracer-setGraphKey(plot-xAxis-pixelToCoord(event-pos().x())); plot-replot(); }); plot-replot(); }10.2 多 Y 轴股票图// 多轴价格 成交量 void MainWindow::setupMultiAxis(QCustomPlot *plot) { // AxisRect 已有默认的下左轴 // 添加右侧 Y 轴成交量 plot-axisRect()-addAxis(QCPAxis::atRight); QCPAxis *rightAxis plot-axisRect()-axis(QCPAxis::atRight, 0); rightAxis-setLabel(Volume); rightAxis-setRange(0, 1e9); // 添加第二条折线成交量柱状图 QCPBars *volumeBars new QCPBars(rightAxis, plot-xAxis); volumeBars-setWidth(0.8); volumeBars-setPen(Qt::NoPen); volumeBars-setBrush(QColor(100, 180, 110, 150)); volumeBars-setData(volumeX, volumeY); // 同步 X 轴缩放K 线和成交量同步缩放 connect(plot-xAxis, QCPAxis::rangeChanged, plot-xAxis, [](const QCPRange newRange) { rightAxis-blockSignals(true); rightAxis-setRange(newRange); // 可选固定范围 rightAxis-blockSignals(false); }); }10.3 日期时间 K 线图void MainWindow::setupFinancialChart() { // 生成 K 线数据OHLC QVectordouble time, open, high, low, close; const double secondsPerDay 86400; double startTime QDateTime(QDate(2024, 1, 1)).toSecsSinceEpoch(); for (int i 0; i 500; i) { double t startTime i * secondsPerDay; double base 100.0 qrand() / double(RAND_MAX) * 20 - 10; double o base; double h base qrand() / double(RAND_MAX) * 5; double l base - qrand() / double(RAND_MAX) * 5; double c base qrand() / double(RAND_MAX) * 10 - 5; time.append(t); open.append(o); high.append(h); low.append(l); close.append(c); } // 创建 K 线图 QCPFinancial *candlesticks new QCPFinancial(plot-xAxis, plot-yAxis); candlesticks-setName(K Line); candlesticks-setChartStyle(QCPFinancial::csCandlestick); // 转换为 OHLC 数据 candlesticks-data()-set( QCPFinancial::timeSeriesToOhlc(time, close, 1, startTime) ); // 涨红跌绿 candlesticks-setTwoColored(true); candlesticks-setBrushPositive(QBrush(QColor(239, 83, 80))); // 红色 candlesticks-setBrushNegative(QBrush(QColor(38, 166, 154))); // 绿色 candlesticks-setWidth(0.8); // 设置日期时间 X 轴 plot-xAxis-setTicker(QSharedPointerQCPAxisTickerDateTime(new QCPAxisTickerDateTime)); plot-xAxis-setDateTimeFormat(yyyy-MM-dd); plot-xAxis-setTickLabelRotation(30); // 标签旋转避免重叠 plot-rescaleAxes(); plot-replot(); }10.4 自定义图表项动态十字光标// 自定义 Item实时跟随鼠标的十字光标 class CrosshairItem : public QCPAbstractItem { Q_OBJECT public: explicit CrosshairItem(QCustomPlot *parent) : QCPAbstractItem(parent) { setSelectable(false); // 不可选中 } // 虚函数重写 void draw(QCPPainter *painter) override { if (!mVisible) return; double x mKeyAxis-coordToPixel(mKey); double y mValueAxis-coordToPixel(mValue); QPen pen(Qt::gray, 1, Qt::DashLine); painter-setPen(pen); // 横向线 painter-drawLine( mKeyAxis-range().lower mKey mKey mKeyAxis-range().upper ? mKeyAxis-axisRect()-left() : x, y, mKeyAxis-range().lower mKey mKey mKeyAxis-range().upper ? mKeyAxis-axisRect()-right() : x, y ); // 竖向线 painter-drawLine(x, mValueAxis-range().lower mValue mValue mValueAxis-range().upper ? mValueAxis-axisRect()-bottom() : y, x, mValueAxis-range().lower mValue mValue mValueAxis-range().upper ? mValueAxis-axisRect()-top() : y ); // 显示坐标值 QString label QString((%1, %2)) .arg(mKey, 0, f, 2) .arg(mValue, 0, f, 2); painter-setFont(QFont(Consolas, 9)); painter-drawText(QPointF(x 5, y - 5), label); } double selectTest(const QPointF , bool, QVariant ) const override { return 0; // 不响应选中 } public: double mKey 0; double mValue 0; bool mVisible true; }; // 使用 CrosshairItem *crosshair new CrosshairItem(plot); connect(plot, QCustomPlot::mouseMove, this, [](QMouseEvent *event) { crosshair-mKey plot-xAxis-pixelToCoord(event-pos().x()); crosshair-mValue plot-yAxis-pixelToCoord(event-pos().y()); plot-replot(QCustomPlot::rpQueuedReplot); });十一、性能优化11.1 大数据量优化策略// 策略 1数据抽稀Douglas-Peucker 算法 void downsampleData(const QVectorQPointF input, QVectorQPointF output, double epsilon) { if (input.size() 3) { output input; return; } // 找到距离首尾连线最远的点 double maxDist 0; int maxIndex 0; QLineF refLine(input.first(), input.last()); for (int i 1; i input.size() - 1; i) { double dist refLine.distance(input[i]); if (dist maxDist) { maxDist dist; maxIndex i; } } if (maxDist epsilon) { QVectorQPointF left, right; downsampleData(input.mid(0, maxIndex 1), left, epsilon); downsampleData(input.mid(maxIndex), right, epsilon); output left right.mid(1); } else { output.append(input.first()); output.append(input.last()); } } // 策略 2QCPAbstractPlottable 自带的线型选择 graph-setLineStyle(QCPGraph::lsLine); // 普线最快 graph-setLineStyle(QCPGraph::lsStepLeft); // 阶梯线 graph-setLineStyle(QCPGraph::lsStepRight); graph-setLineStyle(QCPGraph::lsImpulse); // 脉冲线只画竖线 // 策略 3关闭抗锯齿大幅提升速度牺牲画质 plot-setNotAntialiasedElements(QCP::aePlottables); painter-setRenderHint(QPainter::Antialiasing, false); // 策略 4使用 QCPGraph::setAdaptiveSampling自适应采样 graph-setAdaptiveSampling(true); // 默认开启可见像素点不超过分辨率 // 策略 5最小更新模式 plot-setViewportUpdateMode(QCustomPlot::minimalUpdate); // 默认 // partialUpdate: 只更新变化区域 // fullUpdate: 全量重绘最慢11.2 实时数据滚动// 实时滚动 K 线固定窗口不增长数据量 class RealtimeChart : public QObject { Q_OBJECT public: static const int MaxPoints 500; // 固定窗口大小 RealtimeChart(QCustomPlot *plot) : m_plot(plot), m_dataIndex(0) { m_graph m_plot-addGraph(); m_graph-setPen(QPen(Qt::blue, 1.5)); m_plot-setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); } void appendData(double value) { if (m_x.size() MaxPoints) { // 滚动删除最旧的一个 m_x.removeFirst(); m_y.removeFirst(); for (double x : m_x) x - 1; } m_x.append(m_dataIndex); m_y.append(value); m_graph-setData(m_x, m_y); // 动态缩放 Y 轴 auto [minIt, maxIt] std::minmax_element(m_y.begin(), m_y.end()); double margin (*maxIt - *minIt) * 0.1; m_plot-yAxis-setRange(*minIt - margin, *maxIt margin); // 只重绘新数据点不重绘全图 m_graph-rescaleValueAxis(false); m_plot-replot(QCustomPlot::rpQueuedReplot); } private: QCustomPlot *m_plot; QCPGraph *m_graph; QVectordouble m_x, m_y; double m_dataIndex; };十二、源码阅读路线图qcustomplot.h: QCustomPlot ← 主控件 QCPPainter ← 扩展 QPainter QCPLayoutElement ← 布局基类 QCPLayoutGrid ← 多图网格布局 QCPAxis ← 坐标轴 QCPAxisTicker ← 刻度生成器 QCPAxisTickerDateTime ← 时间轴 QCPAbstractPlottable ← 图表基类 QCPGraph ← 折线图 QCPFinancial ← K 线图 QCPBars ← 柱状图 QCPColorMap ← 热力图 QCPAbstractItem ← 装饰项基类 QCPRange ← 坐标范围 qcustomplot.cpp: 1. QCustomPlot::paintEvent ← 绘图入口 2. QCustomPlot::paint ← 三层渲染 3. QCPAxis::coordToPixel ← 坐标转换 4. QCPGraph::draw ← 折线渲染 5. QCPFinancial::draw ← K 线渲染 6. QCPAxisTickerDateTime::calcTickStep ← 时间刻度 7. QCPDataContainer::getVisibleData ← 可见数据筛选 8. QCustomPlot::mouseEvent ← 交互事件处理关键断点建议// 在调试器里设这些断点 // 1. QCustomPlot::paintEvent ← 绘图总入口可以看调用频率 // 2. QCPAxis::coordToPixel ← 坐标转换性能热点 // 3. QCPGraph::draw ← 折线渲染数据量大时性能瓶颈 // 4. QCPPainter::drawText ← 文本绘制最耗时的绘制操作 // 5. QCPDataContainer::getVisibleData ← 数据筛选抽稀策略验证结语QCustomPlot 的源码是学习 Qt 绘图的最好教材底层是 QPainter— 一切皆 drawLine/drawRect/drawText核心是坐标转换—coordToPixel/pixelToCoord贯穿始终数据管理靠 QCPDataContainer— 可见数据筛选是性能关键交互靠事件分发—mouseEvent/wheelEvent判断交互标志后处理刻度生成靠策略模式—QCPAxisTicker的子类实现不同的刻度策略上手用QCustomPlot啃源码学QPainterQWidget事件模型一举两得。本文基于 QCustomPlot 2.1.1 源码。QCustomPlot 为单文件header-only 部分在 .himplementation 在 .cpp无需额外编译拖入项目即可使用。注若有发现问题欢迎大家提出来纠正

相关文章:

QCustomPlot 深度解析:从渲染架构到源码内幕

一、QCustomPlot 是什么,不是什么QCustomPlot 是一个 Qt 绘图库,核心就两个文件:qcustomplot.h qcustomplot.cpp。不是 Qt 官方库,不属于 Qt 模块,但做得比 Qt Charts 干净得多。设计哲学:扩展 Qt 的 QPai…...

云PDM——制造业研发数据管理的“降维打击”与国产突围

提到最让中国人骄傲的两个产业,非制造业和互联网莫属。当这两者发生深度化学反应时,真正落地的绝不是空泛的概念,而是实打实的技术赋能。在这波浪潮中,云PDM(产品数据管理)绝对算得上是搅动制造业研发端的一…...

创建私有云主机

1. 环境准备与规划在搭建IaaS平台之前,合理的硬件与网络规划是成功的关键。本环境基于VMware Workstation搭建,采用双节点架构。1.1 硬件资源配置请严格按照以下标准配置虚拟机,资源不足会导致安装失败或运行卡顿。表格组件内存处理器硬盘网卡…...

C# OnnxRuntime 部署 RMBG-2.0 实现高精度背景去除

目录 说明 RMBG-2.0 是什么 BiRefNet 架构的核心思想 效果 模型信息 项目 代码 下载 模型下载 说明 背景去除是图像处理中的一个经典难题。从早期的颜色键控、GrabCut,到如今基于深度学习的分割模型,技术的演进让抠图这件事变得越来越智能。而…...

OpenClaw备份策略:gemma-3-12b-it自动化数据保护方案

OpenClaw备份策略:gemma-3-12b-it自动化数据保护方案 1. 为什么需要AI驱动的自动化备份? 上个月我的移动硬盘突然罢工,导致三个月的项目文档全部丢失。这次惨痛经历让我意识到:传统备份方案存在两个致命缺陷——依赖人工记忆和缺…...

OpenClaw免费模型推荐与配置指南!

OpenClaw免费模型推荐与配置指南 OpenClaw(小龙虾)作为开源 AI 智能体框架,本身不内置大模型,而是支持灵活对接各类 AI 服务。本文整理了 2026 年最新的完全免费、好用稳定的模型方案,涵盖云端 API 和本地私有化部署,附带详细的配置步骤,帮你零成本玩转 OpenClaw。 一…...

主键、外键和约束:让数据库“有规矩”才能不出错!|转行学DB第5天

为什么你的表里会混进“奇怪的数据”?三分钟搞懂数据库的“家规”大家好呀!我是数据库小学妹👋一个正在从设计转行学数据库的"萌新"。 前几篇我们学会了建表、插数据、查数据。但有个问题一直让我头疼:我怎么保证同一张…...

第七届全球校园人工智能算法精英大赛-算法巅峰赛产业命题赛第3赛季优化题--多策略混合算法

前言 全球校园人工智能算法精英大赛”是江苏省人工智能学会举办的面向全球具有正式学籍的全日制高等院校及以上在校学生举办的算法竞赛。其中的算法巅峰赛属于产业命题赛道,这是第3赛季,这次优化题的主题是 “碳中和”。 回顾 第七届全球校园人工智能算…...

鸿蒙_ArkUI组件同时支持双击和单击事件

我们常用的点击事件是onClick,想要实现双击需要用TapGesture手势实现,那么如果一个组件同时需要支持单击和双击,则需要使用GestureGroup,我们新建一个页面来测试一下:Entry Component struct TestDoubleClick {State m…...

动态规划——01背包问题、完全背包(python、一维DP)

01-背包问题:从最大容量开始,从后往前遍历背包容量每种物品只能选择一次。物品种类为n,背包容量为k。从最大容量开始,从后往前遍历背包容量,小于当前物品容量的背包大小不遍历,即遍历到w[i]即可。&#xff…...

3.一文看懂反向传播:从单个神经元到 PyTorch 自动求导

反向求导,多层次对应一个神经,单个神经元场景 学习这一篇的前提是,已经学会了梯度算法和线性结构算法,不明白的可以去看我之前的文章。 前面看不懂的,直接跳转到 “ 反向传播的流程 ” 底层的数学算法 z 是中间变量 u …...

【国家卫健委《医疗卫生机构数据安全管理指南》强制落地倒计时】:PHP脱敏工具未升级?3类高危场景已触发监管预警!

第一章:国家卫健委《医疗卫生机构数据安全管理指南》强制落地背景与合规红线近年来,医疗健康数据泄露事件频发,患者隐私保护压力陡增。2023年12月,国家卫生健康委员会正式印发《医疗卫生机构数据安全管理指南》(国卫办…...

string的特性及使用

string这个词很容易让我们联想到str,也就是字符串,实际上string和字符串的关联性还是很强的。 很多字符串的题目都是string类的形式出现的,日常工作中为了方便使用都是用的string类, 标准string类 使用string类时,必须…...

(文档)第121讲:Oracle兼容工具—orafce使用技巧

目标 • orafce概述 • orafce安装 • orafce应用案例 orafce概述 orafce(Oracle Functions for PostgreSQL)是一个开源项目,旨在为 PostgreSQL 数据库提供一些 Oracle 数据库中缺失或行为不同的函数。该项目通过在 PostgreSQL 中实现这些函数,帮助…...

QGIS之四十三python处理数据

1、调出Python控制台 2、新建python脚本 右边的窗口可以拉过来 3、让AI根据你的需求写python脚本 比如要进行文本转shp,让AI写python脚本,拷贝脚本进来,保存文件 4、执行脚本...

工业以太网无线网桥 SG-WX-Bridge v2.0|免布线、一对多、即插即用,工业现场无线通信神器

工厂布线麻烦、距离远、施工成本高?设备移动频繁、有线网扯来扯去易损坏?三格电子SG-WX-Bridge v2.0 工业以太网无线网桥,专为工业现场打造,把有线网变无线,1 台 AP 最多带 8 台 STA,Profinet/EtherNet/IP/…...

机器学习经验总结整理

1.一个标准的机器学习项目流程(一定要记住顺序)很多新手觉得乱,是因为没按流程想问题。做任何项目,脑子里要有这根“流水线”:定义问题 → 是分类还是回归还是聚类?业务目标是什么?获取数据 → …...

千问3.5-27B模型微调实战:提升OpenClaw任务成功率

千问3.5-27B模型微调实战:提升OpenClaw任务成功率 1. 为什么需要微调千问3.5-27B? 当我第一次将OpenClaw接入千问3.5-27B时,发现它在简单任务上表现不错,但遇到复杂操作链时经常"卡壳"。比如让它整理一周的会议录音文…...

【国家级数字农业项目技术白皮书节选】:PHP轻量化时序数据处理框架如何扛住每秒8700+传感器上报?

第一章:农业 PHP 物联网数据可视化案例在智慧农业实践中,PHP 作为轻量级服务端语言,常被用于快速构建物联网数据聚合与可视化看板。本案例基于 ESP32 传感器节点采集土壤湿度、环境温湿度及光照强度,通过 HTTP POST 将 JSON 数据推…...

Arduino RTCtime库:标准time.h兼容的DS1307/DS3231驱动

1. 项目概述RTCtime 是一款专为 Arduino 平台设计的实时时钟(RTC)驱动库,核心目标是在硬件 RTC 模块与标准 C 运行时时间系统之间建立语义一致、类型兼容的桥梁。它并非一个独立的时间计算引擎,而是对底层硬件寄存器操作的封装层&…...

2026年全网视频去水印实测:6款消除字幕工具上手,哪款更适合你

短视频剪辑、素材二创时,画面里的顽固字幕、平台角标总是破坏整体质感,找对去字幕工具能直接拉高成品效率。这次我们亲测了市面上 6 款关注度较高的视频消除字幕工具,从便捷性、处理效果、隐私安全、批量能力、平台兼容五个维度逐一拆解&…...

加入csdn 5周年

不知不觉,已经是5年过去了,今天在b站刷了个视频大有感触,决定也用csdn记录一点东西,而不是一直把东西放在github上面或者是本地...

Idiap研究院:让语音识别AI学会聆听对话历史,压缩音频记忆

语音识别技术在我们的生活中越来越常见,从手机语音助手到客服电话,从会议记录到智能搜索。但你有没有想过,为什么这些系统有时候会犯一些很明显的错误?比如当你在和语音助手对话时,明明在前一句话中提到了"张三&q…...

方差的数学意义

方差(Variance)是统计学中一个非常基础且核心的概念。简单来说,它的数学意义就是衡量一组数据的离散程度,或者叫波动性。 如果说“平均值”(均值)告诉我们数据的中心在哪里,那么“方差”告诉我们…...

从月损耗20万到年增收300万:零售老板180天蜕变

李老板在二线城市经营着5家社区超市,年营收5000万,利润却越来越薄。一个残酷的数字让他夜不能寐:因线上线下系统割裂、手工操作繁多,每月生鲜损耗和运营错漏造成的损失超过20万元。他意识到,不变革,就是在慢…...

OpenClaw+Qwen3.5-9B组合优势:3个不可替代的使用场景

OpenClawQwen3.5-9B组合优势:3个不可替代的使用场景 1. 为什么选择OpenClawQwen3.5-9B组合 去年夏天,当我第一次尝试用Python脚本自动化处理医疗研究数据时,遇到了一个尴尬的问题:要么忍受公有云API的数据隐私风险,要…...

GLM-. 全面支持与 Gemini CLI 集成:HagiCode 的多模型进化之路估

1. 流图:数据的河流 如果把传统的堆叠面积图想象成一块块整齐堆叠的积木,那么流图就像一条蜿蜒流淌的河流,河道的宽窄变化自然流畅,波峰波谷过渡平滑。 它特别适合展示多个类别数据随时间的变化趋势,尤其是当你想强调整…...

使用Dify对接自己开发的mcp

先要有自己开发的mcp,然后部署到服务器 如何开发自己的mcp:Java使用spring Ai集成的mcp开发自己的mcp-CSDN博客 使用Dify对接mcp服务 Dify如何接入MCP工具_dify如何使用mcp工具-CSDN博客 基本上都差不多, 只说一点:如果一直报…...

eVTOL 研制必读 | 厘清研制保证与设计保证的边界

在很多航空企业里,经常会出现一种现象:项目团队在谈“研制保证体系”,管理层在谈“设计保证系统”;技术人员在强调 ARP4754A/B,组织层面却在说 DOA 合规。大家都在讲“保证”,却未必在讲同一件事。结果是什…...

无人外卖店

本项目以智能无人柜云值守模式,布局写字楼、社区等外卖密集区,24小时运营,对接美团、饿了么平台,主打预制餐饮品、零食等标准化商品。...