基于QCustomPlot实现色条(ColorBar)

一、简介

通过QCustomPlot实现ColorBar,直观显示各个位置的异常情况。实现效果如下,
在这里插入图片描述

二、源码

  1. CPColorBar.hpp
// CPColorBar.hpp
#pragma once
#include "qcustomplot.h"

#include <QHash>

class QCP_LIB_DECL CPColorBarData {
public:
    CPColorBarData() : key(0), value(0) {}

    CPColorBarData(double key, double value) : key(key), value(value) {}

    inline double sortKey() const {
        return key;
    }
    inline static CPColorBarData fromSortKey(double sortKey) {
        return CPColorBarData{sortKey, 0};
    }
    inline static bool sortKeyIsMainKey() {
        return true;
    }

    inline double mainKey() const {
        return key;
    }
    inline double mainValue() const {
        return value;
    }

    inline QCPRange valueRange() const {
        return QCPRange{value, value};
    }

    double key, value;
};
Q_DECLARE_TYPEINFO(CPColorBarData, Q_PRIMITIVE_TYPE);


typedef QCPDataContainer<CPColorBarData> QCPColorBarDataContainer;

class QCP_LIB_DECL CPColorBar : public QCPAbstractPlottable1D<CPColorBarData> {
    Q_OBJECT
    Q_PROPERTY(double width READ width WRITE setWidth)

public:
    explicit CPColorBar(QCPAxis* keyAxis, QCPAxis* valueAxis);
    virtual ~CPColorBar() Q_DECL_OVERRIDE;

    // getters
    double width() const {
        return mWidth;
    }
    QSharedPointer<QCPColorBarDataContainer> data() const {
        return mDataContainer;
    }

    // setters
    void setData(QSharedPointer<QCPColorBarDataContainer> data);
    void setData(const QVector<double>& keys, const QVector<double>& values, bool alreadySorted = false);
    void setWidth(double width);
    void setColor(const QHash<int, QColor>& color);

    // 非属性方法
    void addData(const QVector<double>& keys, const QVector<double>& values, bool alreadySorted = false) const;
    void addData(double key, double value);

    // 重新实现的虚方法
    virtual QCPDataSelection selectTestRect(const QRectF& rect, bool onlySelectable) const Q_DECL_OVERRIDE;
    virtual double selectTest(
        const QPointF& pos, bool onlySelectable, QVariant* details = nullptr) const Q_DECL_OVERRIDE;
    virtual QCPRange getKeyRange(bool& foundRange, QCP::SignDomain inSignDomain = QCP::sdBoth) const Q_DECL_OVERRIDE;
    virtual QCPRange getValueRange(bool& foundRange, QCP::SignDomain inSignDomain = QCP::sdBoth,
        const QCPRange& inKeyRange = QCPRange()) const Q_DECL_OVERRIDE;
    virtual QPointF dataPixelPosition(int index) const Q_DECL_OVERRIDE;

protected:
    // 属性成员
    double mWidth;
    QHash<int, QColor> mColor;

    virtual void draw(QCPPainter* painter) Q_DECL_OVERRIDE;
    virtual void drawLegendIcon(QCPPainter* painter, const QRectF& rect) const Q_DECL_OVERRIDE;

    void getVisibleDataBounds(
        QCPColorBarDataContainer::const_iterator& begin, QCPColorBarDataContainer::const_iterator& end) const;

    friend class QCustomPlot;

private:
    void getOptimizedBarData(QVector<CPColorBarData>* barData, const QCPColorBarDataContainer::const_iterator& begin,
        const QCPColorBarDataContainer::const_iterator& end) const;

    bool mAdaptiveSampling; // 自适应采样
};

  1. CPColorBar.cpp
// CPColorBar.cpp
#include "CPColorBar.hpp"

#include <algorithm>
#include <limits>

CPColorBar::CPColorBar(QCPAxis* keyAxis, QCPAxis* valueAxis)
    : QCPAbstractPlottable1D<CPColorBarData>(keyAxis, valueAxis), mWidth{1}, mAdaptiveSampling{true} {
    // 修改从抽象绘图表继承的属性
    mPen.setColor(Qt::blue);
    mPen.setStyle(Qt::SolidLine);
    mBrush.setColor(QColor(40, 50, 255, 30));
    mBrush.setStyle(Qt::SolidPattern);
    mSelectionDecorator->setBrush(QBrush(QColor(160, 160, 255)));
}

CPColorBar::~CPColorBar() = default;

void CPColorBar::setData(QSharedPointer<QCPColorBarDataContainer> data) {
    mDataContainer = data;
}

void CPColorBar::setData(const QVector<double>& keys, const QVector<double>& values, bool alreadySorted) {
    mDataContainer->clear();
    addData(keys, values, alreadySorted);
}

// 设置条的宽度
void CPColorBar::setWidth(double width) {
    mWidth = width;
}

void CPColorBar::setColor(const QHash<int, QColor>& color) {
    mColor = color;
}

void CPColorBar::addData(const QVector<double>& keys, const QVector<double>& values, bool alreadySorted)const {
    if (keys.size() != values.size())
        qDebug() << Q_FUNC_INFO << "keys and values have different sizes:" << keys.size() << values.size();
    const int n = qMin(keys.size(), values.size());
    QVector<CPColorBarData> tempData(n);
    QVector<CPColorBarData>::iterator it = tempData.begin();
    const QVector<CPColorBarData>::iterator itEnd = tempData.end();
    int i = 0;
    while (it != itEnd) {
        it->key = keys[i];
        it->value = values[i];
        ++it;
        ++i;
    }
    mDataContainer->add(tempData, alreadySorted); // 请勿修改 tempData 以防止写入时复制
}

void CPColorBar::addData(double key, double value) {
    mDataContainer->add(CPColorBarData(key, value));
}

QCPDataSelection CPColorBar::selectTestRect(const QRectF& rect, bool onlySelectable) const {
    QCPDataSelection result;
    return result;
}

double CPColorBar::selectTest(const QPointF& pos, bool onlySelectable, QVariant* details) const {
    return -1;
}

QCPRange CPColorBar::getKeyRange(bool& foundRange, QCP::SignDomain inSignDomain) const {
    return mDataContainer->keyRange(foundRange, inSignDomain);
}

QCPRange CPColorBar::getValueRange(bool& foundRange, QCP::SignDomain inSignDomain, const QCPRange& inKeyRange) const {
    return mDataContainer->valueRange(foundRange, inSignDomain, inKeyRange);
}

QPointF CPColorBar::dataPixelPosition(int index) const {
    if (index >= 0 && index < mDataContainer->size()) {
        QCPAxis* keyAxis = mKeyAxis.data();
        QCPAxis* valueAxis = mValueAxis.data();
        if (!keyAxis || !valueAxis) {
            qDebug() << Q_FUNC_INFO << "invalid key or value axis";
            return {};
        }

        const QCPDataContainer<CPColorBarData>::const_iterator it = mDataContainer->constBegin() + index;
        const double valuePixel = valueAxis->coordToPixel(it->value);
        const double keyPixel = keyAxis->coordToPixel(it->key);
        if (keyAxis->orientation() == Qt::Horizontal)
            return {keyPixel, valuePixel};
        else
            return {valuePixel, keyPixel};
    } else {
        qDebug() << Q_FUNC_INFO << "Index out of bounds" << index;
        return {};
    }
}

/* 从基类继承文档 */
void CPColorBar::draw(QCPPainter* painter) {
    if (!mKeyAxis || !mValueAxis) {
        qDebug() << Q_FUNC_INFO << "invalid key or value axis";
        return;
    }
    if (mDataContainer->isEmpty())
        return;

    QCPColorBarDataContainer::const_iterator begin, end;
    getVisibleDataBounds(begin, end);

    // 绘制片段
    mDataContainer->limitIteratorsToDataRange(begin, end, QCPDataRange(0, dataCount()));

    if (begin == end || begin + 1 == end)
        return;

    QVector<CPColorBarData> barData;

    getOptimizedBarData(&barData, begin, end);
    if (barData.empty())
        return;

    begin = barData.constBegin();
    end = barData.constEnd();

    QCPAxis* keyAxis = mKeyAxis.data();
    QCPAxis* valueAxis = mValueAxis.data();
    const QCPRange valueRange = valueAxis->range();
    const int top = valueAxis->coordToPixel(valueRange.upper);
    const int bottom = valueAxis->coordToPixel(valueRange.lower);
    double currentStartKey{begin->key};
    int currentStartValue{static_cast<int>(begin->value)};
    for (QCPColorBarDataContainer::const_iterator it = begin; it != end; ++it) {
        // 如果设置了标志,则检查数据有效性:
#ifdef QCUSTOMPLOT_CHECK_DATA
        if (QCP::isInvalidData(it->key, it->value))
            qDebug() << Q_FUNC_INFO << "Data point at" << it->key << "of drawn range invalid."
                     << "Plottable name:" << name();
#endif
        const bool value_changed = static_cast<int>(it->value) != currentStartValue;
        if (value_changed || it + 1 == end) {
            const double endMiddleKey = value_changed ? ((it - 1)->key + it->key) * 0.5 : it->key; // 取跃变的中间位置
            // 着色绘制
            const int startPix = keyAxis->coordToPixel(currentStartKey);
            const int endPix = keyAxis->coordToPixel(endMiddleKey);

            if (endPix - startPix >= 1) {
                const auto iter = mColor.find(currentStartValue);
                const QColor color = iter != mColor.cend() ? iter.value() : Qt::black;
                painter->setPen(color);
                painter->setBrush(QBrush(color));
                painter->drawRect(startPix, top, endPix - startPix, bottom - top);
            }

            currentStartKey = endMiddleKey;
            currentStartValue = static_cast<int>(it->value);

            if (value_changed && it + 1 == end) {
                --it;
            }
        }
    }

    // 绘制不只是线条/散点笔和画笔的其他选择装饰
    if (mSelectionDecorator)
        mSelectionDecorator->drawDecoration(painter, selection());
}

void CPColorBar::getOptimizedBarData(QVector<CPColorBarData>* barData,
    const QCPColorBarDataContainer::const_iterator& begin, const QCPColorBarDataContainer::const_iterator& end) const {
    if (!barData)
        return;
    QCPAxis* keyAxis = mKeyAxis.data();
    QCPAxis* valueAxis = mValueAxis.data();
    if (!keyAxis || !valueAxis) {
        qDebug() << Q_FUNC_INFO << "invalid key or value axis";
        return;
    }
    if (begin == end)
        return;

    const int dataCount = static_cast<int>(end - begin);
    int maxCount = (std::numeric_limits<int>::max)();
    if (mAdaptiveSampling) {
        // 所选key区间所对应的像素宽度
        const double keyPixelSpan = qAbs(keyAxis->coordToPixel(begin->key) - keyAxis->coordToPixel((end - 1)->key));
        if (2 * keyPixelSpan + 2 < static_cast<double>((std::numeric_limits<int>::max)()))
            maxCount = static_cast<int>(2 * keyPixelSpan + 2); // 最大点数为区间像素的两倍
    }

    barData->reserve(maxCount * 1.05);
    if (mAdaptiveSampling && dataCount >= maxCount) { // 仅当平均每个像素至少有两个点时才使用自适应采样
        QCPColorBarDataContainer::const_iterator it = begin;
        double maxValue = it->value;
        QCPColorBarDataContainer::const_iterator currentIntervalFirstPoint = it;
        const int reversedFactor = keyAxis->pixelOrientation(); // 用于计算 keyEpsilon 像素到正确的方向
        const int reversedRound = reversedFactor == -1 ? 1 : 0;
        double currentIntervalStartKey =
            keyAxis->pixelToCoord(static_cast<int>(keyAxis->coordToPixel(begin->key) + reversedRound));
        double lastIntervalEndKey = currentIntervalStartKey;
        // 映射到绘图键坐标时屏幕上一个像素的间隔
        double keyEpsilon =
            qAbs(currentIntervalStartKey
                 - keyAxis->pixelToCoord(keyAxis->coordToPixel(currentIntervalStartKey) + 1.0 * reversedFactor));
        // 指示是否需要在每个间隔后更新 keyEpsilon(对于对数轴)
        const bool keyEpsilonVariable = keyAxis->scaleType() == QCPAxis::stLogarithmic;
        int intervalDataCount = 1;
        ++it; // 将迭代器提前到第二个数据点,因为自适应采样在 1 点回溯中起作用
        while (it != end) {
            // 数据点仍在同一像素内,因此请跳过它并在必要时扩展此集群的值范围
            if (it->key < currentIntervalStartKey + keyEpsilon) {
                if (it->value > maxValue)
                    maxValue = it->value;
                ++intervalDataCount;
            } else { // 新的像素间隔开始
                if (intervalDataCount >= 2) { // 最后一个像素有多个数据点,将它们合并到一个集群中
                    // 最后一个点更远,所以这个集群的第一个点必须是一个真实的数据点
                    if (lastIntervalEndKey < currentIntervalStartKey - keyEpsilon)
                        barData->append(CPColorBarData(
                            currentIntervalStartKey + keyEpsilon * 0.2, currentIntervalFirstPoint->value));
                    barData->append(CPColorBarData(currentIntervalStartKey + keyEpsilon * 0.75, maxValue));
                    // 新像素开始远离前一个集群,因此请确保集群的最后一个点位于真实数据点
                    if (it->key > currentIntervalStartKey + keyEpsilon * 2)
                        barData->append(CPColorBarData(currentIntervalStartKey + keyEpsilon * 0.8, (it - 1)->value));
                } else
                    barData->append(CPColorBarData(currentIntervalFirstPoint->key, currentIntervalFirstPoint->value));
                lastIntervalEndKey = (it - 1)->key;
                maxValue = it->value;
                currentIntervalFirstPoint = it;
                currentIntervalStartKey =
                    keyAxis->pixelToCoord(static_cast<int>(keyAxis->coordToPixel(it->key) + reversedRound));
                if (keyEpsilonVariable)
                    keyEpsilon = qAbs(
                        currentIntervalStartKey
                        - keyAxis->pixelToCoord(keyAxis->coordToPixel(currentIntervalStartKey) + 1.0 * reversedFactor));
                intervalDataCount = 1;
            }
            ++it;
        }
        // 处理最后一个间隔
        if (intervalDataCount >= 2) { // 最后一个像素有多个数据点,将它们合并到一个集群中
            // 最后一个点不是一个集群,所以这个集群的第一个点必须是一个真实的数据点
            if (lastIntervalEndKey < currentIntervalStartKey - keyEpsilon)
                barData->append(
                    CPColorBarData(currentIntervalStartKey + keyEpsilon * 0.2, currentIntervalFirstPoint->value));
            barData->append(CPColorBarData(currentIntervalStartKey + keyEpsilon * 0.75, maxValue));
        } else
            barData->append(CPColorBarData(currentIntervalFirstPoint->key, currentIntervalFirstPoint->value));
    } else { // 不使用自适应采样算法,将点从数据容器一对一传输到输出
        barData->resize(dataCount);
        std::copy(begin, end, barData->begin());
    }
}

/* 从基类继承文档 */
void CPColorBar::drawLegendIcon(QCPPainter* painter, const QRectF& rect) const {
    // 绘制填充矩形:
    applyDefaultAntialiasingHint(painter);
    painter->setBrush(mBrush);
    painter->setPen(mPen);
    QRectF r = QRectF(0, 0, rect.width() * 0.67, rect.height() * 0.67);
    r.moveCenter(rect.center());
    painter->drawRect(r);
}

/*!  内部的

  由draw调用以确定在当前键轴范围设置下哪个数据(键)范围可见,因此只需要处理。 它还考虑了条形宽度。

  begin 返回一个迭代器,指向绘图时需要考虑的最低数据点。 请注意,为了得到一个干净的绘图,
  一直到轴矩形的边缘,较低的可能仍然刚好在可见范围之外。

  end 返回一个比最高可见数据点高一的迭代器。 和以前一样,end 也可能位于可见范围之外。

  如果绘图表不包含数据,则起点和终点都指向 constEnd。
*/
void CPColorBar::getVisibleDataBounds(
    QCPColorBarDataContainer::const_iterator& begin, QCPColorBarDataContainer::const_iterator& end) const {
    if (!mKeyAxis) {
        qDebug() << Q_FUNC_INFO << "invalid key axis";
        begin = mDataContainer->constEnd();
        end = mDataContainer->constEnd();
        return;
    }
    if (mDataContainer->isEmpty()) {
        begin = mDataContainer->constEnd();
        end = mDataContainer->constEnd();
        return;
    }

    QCPAxis* keyAxis = mKeyAxis.data();
    QCPAxis* valueAxis = mValueAxis.data();
    // 获取可见数据范围
    begin = mDataContainer->findBegin(keyAxis->range().lower);
    end = mDataContainer->findEnd(keyAxis->range().upper);
    // 将下限/上限限制为 rangeRestriction
    // mDataContainer->limitIteratorsToDataRange(begin, end, QCPDataRange(0, dataCount()));
}

三、示例

#include "CPColorBar.hpp"

#include <QApplication>

int main(int argc, char* argv[]) {
    QApplication a(argc, argv);

    // 生成数据
    QVector<double> x{
        -1, -0.9, -0.8, -0.7, -0.6, -0.5, -0.4, -0.3, -0.2, -0.1, 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0};
    QVector<double> y{0, 3, 2, 1, 0, 0, 3, 0, 3, 0, 2, 2, 2, 0, 0, 1, 0, 1, 0, 1, 0};
    // 创建图表
    QCustomPlot* customPlot = new QCustomPlot();
    auto* color_bar = new CPColorBar(customPlot->xAxis, customPlot->yAxis);
    color_bar->setColor({{0, Qt::gray}, {1, Qt::blue}, {2, Qt::yellow}, {3, Qt::red}});
    color_bar->setData(x, y);
    // 设置坐标轴范围
    customPlot->xAxis->setRange(-1, 1);
    customPlot->yAxis->setRange(-1, 1);
    customPlot->yAxis->setVisible(false);
    customPlot->replot();
    customPlot->show();
    customPlot->resize(600, 100);

    return a.exec();
}

运行效果如下:
在这里插入图片描述

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值