自定义的Qt折线图控件

可以在QWidget内绘制折线图。做的比较简单,因为从写到调试只用了大概1天时间。不太智能,但是可以应对一般的场景。在VS2015和Qt5.9上测试可以使用。当然,由于测试不到位里面可能有些未知的BUG。类的接口简单,只有append函数用来添加一条折线,和removeAt函数用来删除折线。每条线的颜色是随机的,所以小概率会用一样的颜色。效果图如下:

下面上代码,头文件:

class MPolyline;

using PaintFunc = void(MPolyline::*)(QPainter*, const QFontMetrics&, const QPointF&, qreal);

class MPolyline : public QWidget
{
    Q_OBJECT

public:
    MPolyline(QWidget* parent = nullptr);
    QRgb append(const QVector<QPointF>& pts);
    void removeAt(int i);

private:
    struct AxisInfo;

    template<typename Func>
    std::pair<QPointF, QPointF> minMaxValue(const QVector<QVector<QPointF>>& points, Func func);

    void paintEvent(QPaintEvent *event) override;
    QRgb obtainColor();
    qreal calcBaseNumber(qreal v);
    int calcDivision(int a, int b);
    void calcAxisXInfo(AxisInfo& xAxis, int &ox);
    void calcAxisYInfo(AxisInfo& yAxis, int &oy);
    void correctApex(qreal& min, qreal& max);
    void huaKeDuX(QPainter* painter, const QFontMetrics& fm, const QPointF& xpt, qreal value);
    void huaKeDuY(QPainter* painter, const QFontMetrics& fm, const QPointF& ypt, qreal value);
    void huaKeDu(QPainter* painter, const QFontMetrics& fm, const AxisInfo& axis, PaintFunc func);
    void huaJiaoChaKeDu(QPainter* painter, const QFontMetrics& fm, const AxisInfo& axis, PaintFunc func);
    void paintPolyline(QPainter* painter, const AxisInfo& xAxis, const AxisInfo& yAxis, int i);
    qreal calcNearNumber(qreal value, bool upperOrLower);

    enum SectionType
    {
        CROSSED,
        A_IS_ZERO,
        B_IS_ZERO,
        LESSER,
        GREATER,
    };

    struct AxisInfo
    {
        SectionType type;
        qreal av;
        qreal bv;
        QPointF a; /* 对应av值的屏幕坐标 */
        QPointF b; /* 对应bv值的屏幕坐标 */
    };

private:
    const static QMargins axism; /* 坐标轴对于控件的边距 */
    const static QMargins datam; /* 坐标轴最大数据范围对坐标轴的边距 */
    QVector<QVector<QPointF>> datas;
    QVector<QRgb> colors;
};

CPP文件:

const QMargins MPolyline::axism(20, 20, 20, 20);
const QMargins MPolyline::datam(20, 20, 20, 20);

MPolyline::MPolyline(QWidget* parent) :
    QWidget(parent)
{
}

QRgb MPolyline::obtainColor()
{
    int r = qrand() % 223 + 32;
    int b = qrand() % 223 + 32;
    int g = 255 - qMin(r, b);
    return qRgb(r, g, b);
}

QRgb MPolyline::append(const QVector<QPointF> &pts)
{
    QRgb col = obtainColor();
    datas.push_back(pts);
    colors.push_back(col);
    update();
    return col;
}

void MPolyline::removeAt(int i)
{
    datas.remove(i);
    colors.remove(i);
    update();
}

qreal MPolyline::calcBaseNumber(qreal v)
{
    qreal i = 1e-34;
    while (qAbs(v / i) >= 1)
    {
        i *= 10;
    }
    i /= 10;
    return i;
}

//---------------------------------------------------------------------------------------
// 计算数值value附近最近的可整除…0.1或1或10或100或1000…的数值
// value:输入数值
// upperorLower : 指示向上取值还是向下取值
// 返回值:返回附近可整除10或100或1000…的数值
//---------------------------------------------------------------------------------------
qreal MPolyline::calcNearNumber(qreal value, bool upperOrLower)
{
    if (value == 0)
    {
        return 0;
    }
    qreal i = calcBaseNumber(value);
    int x = int(value / i);
    qreal y = i * x;
    if (upperOrLower)
    {
        return (value > 0) ? y + i : y;
    }
    else
    {
        return (value > 0) ? y : y - i;
    }
}

void MPolyline::huaKeDuY(QPainter* painter, const QFontMetrics& fm, const QPointF& ypt, qreal value)
{
    const int yTextOffset = 19;
    painter->drawLine(ypt, ypt + QPointF(5, 0));
    QString ty = QString::number(value, 'g', 3);
    QSize tysz = fm.size(0, ty);
    QPoint tyCenter(int(ypt.x() - yTextOffset), int(ypt.y()));
    painter->drawText(QRect(QPoint(tyCenter.x() - tysz.width() / 2, tyCenter.y() - tysz.height() / 2), tysz), ty);
}

void MPolyline::huaKeDuX(QPainter* painter, const QFontMetrics& fm, const QPointF& xpt, qreal value)
{
    const int xTextOffset = 10;
    painter->drawLine(xpt, xpt + QPointF(0, -5));
    QString tx = QString::number(value, 'g', 3);
    QSize txsz = fm.size(0, tx);
    QPoint txCenter(int(xpt.x()), int(xpt.y() + xTextOffset));
    painter->drawText(QRect(QPoint(txCenter.x() - txsz.width() / 2, txCenter.y() - txsz.height() / 2), txsz), tx);
}

void MPolyline::correctApex(qreal& min, qreal& max)
{
    if (min == 0 && max == 0)
    {
        max = 1;
    }
    else if (min > 0 && max / min > 10)
    {
        min = 0;
    }
    else if (max < 0 && min / max > 10)
    {
        max = 0;
    }
    else if (min * max < 0 && max / min < -10)
    {
        min = max / -10;
    }
    else if (min * max < 0 && min / max < -10)
    {
        max = min / -10;
    }
}

template<typename Func>
std::pair<QPointF, QPointF> MPolyline::minMaxValue(const QVector<QVector<QPointF>>& points, Func func)
{
    std::pair<QPointF, QPointF> result;
    if (points.isEmpty())
    {
        return result;
    }
    QVector<QPointF> minmaxs;
    for (auto& arrs : points)
    {
        auto temp = std::minmax_element(arrs.begin(), arrs.end(), func);
        minmaxs.push_back(*temp.first);
        minmaxs.push_back(*temp.second);
    }
    auto apex = std::minmax_element(minmaxs.begin(), minmaxs.end(), func);
    result.first = *apex.first;
    result.second = *apex.second;
    return result;
}

void MPolyline::calcAxisXInfo(AxisInfo& xAxis, int &ox)
{
    auto apexx = minMaxValue(datas,
        [](const QPointF& a, const QPointF& b) { return a.x() < b.x(); });
    qreal minxv = calcNearNumber(apexx.first.x(), false);
    qreal maxxv = calcNearNumber(apexx.second.x(), true);
    correctApex(minxv, maxxv);
    int xAxisStart = axism.left();
    int xAxisEnd = width() - axism.right();
    int xDataStart = xAxisStart + datam.left();
    int xDataEnd = xAxisEnd - datam.right();
    if (minxv > 0 && maxxv > 0)
    {
        ox = xDataStart;
        xAxis.a.setX(xDataStart + (xDataEnd - xDataStart) * 0.1);
        xAxis.b.setX(xDataEnd);
        xAxis.type = GREATER;
    }
    if (minxv == 0 && maxxv > 0)
    {
        ox = xDataStart;
        xAxis.a.setX(xDataStart);
        xAxis.b.setX(xDataEnd);
        xAxis.type = A_IS_ZERO;
    }
    if (minxv < 0 && maxxv > 0)
    {
        ox = int(xDataStart + (xDataEnd - xDataStart) * -minxv / (maxxv - minxv));
        xAxis.a.setX(xDataStart);
        xAxis.b.setX(xDataEnd);
        xAxis.type = CROSSED;
    }
    if (minxv < 0 && maxxv == 0)
    {
        ox = xDataEnd;
        xAxis.a.setX(xDataStart);
        xAxis.b.setX(xDataEnd);
        xAxis.type = B_IS_ZERO;
    }
    if (minxv < 0 && maxxv < 0)
    {
        ox = xDataEnd;
        xAxis.a.setX(xDataStart);
        xAxis.b.setX(xDataEnd + (xDataStart - xDataEnd) * 0.1);
        xAxis.type = LESSER;
    }
    xAxis.av = minxv;
    xAxis.bv = maxxv;
}

void MPolyline::calcAxisYInfo(AxisInfo& yAxis, int &oy)
{
    auto apexy = minMaxValue(datas,
        [](const QPointF& a, const QPointF &b) { return a.y() < b.y(); });
    qreal minyv = calcNearNumber(apexy.first.y(), false);
    qreal maxyv = calcNearNumber(apexy.second.y(), true);
    correctApex(minyv, maxyv);
    int yAxisStart = height() - axism.bottom();
    int yAxisEnd = axism.top();
    int yDataStart = yAxisStart - datam.bottom();
    int yDataEnd = yAxisEnd + datam.top();
    if (minyv > 0 && maxyv > 0)
    {
        oy = yDataStart;
        yAxis.a.setY(yDataStart + (yDataEnd - yDataStart) * 0.1);
        yAxis.b.setY(yDataEnd);
        yAxis.type = GREATER;
    }
    if (minyv == 0 && maxyv > 0)
    {
        oy = yDataStart;
        yAxis.a.setY(yDataStart);
        yAxis.b.setY(yDataEnd);
        yAxis.type = A_IS_ZERO;
    }
    if (minyv < 0 && maxyv> 0)
    {
        oy = int(yDataStart + (yDataEnd - yDataStart)* -minyv / (maxyv - minyv));
        yAxis.a.setY(yDataStart);
        yAxis.b.setY(yDataEnd);
        yAxis.type = CROSSED;
    }
    if (minyv < 0 && maxyv == 0)
    {
        oy = yDataEnd;
        yAxis.a.setY(yDataStart);
        yAxis.b.setY(yDataEnd);
        yAxis.type = B_IS_ZERO;
    }
    if (minyv < 0 && maxyv < 0)
    {
        oy = yDataEnd;
        yAxis.a.setY(yDataStart);
        yAxis.b.setY(yDataEnd + (yDataStart - yDataEnd) * 0.1);
        yAxis.type = LESSER;
    }
    yAxis.av = minyv;
    yAxis.bv = maxyv;
}

void MPolyline::huaKeDu(QPainter* painter, const QFontMetrics& fm, const AxisInfo& axis, PaintFunc func)
{
    /* 坐标轴数值范围由于是10的倍数, 因此能被2和5整除 */
    switch (axis.type)
    {
    case GREATER:
    case LESSER:
        for (int i = 0; i <= 5; i++)
        {
            QPointF keDu(axis.a + (axis.b - axis.a) / 5 * i);
            qreal value = axis.av + (axis.bv - axis.av) / 5 * i;
            (this->*func)(painter, fm, keDu, value);
        }
        break;
    case A_IS_ZERO:
        for (int i = 0; i <= 4; i++)
        {
            QPointF keDu(axis.b + (axis.a - axis.b) / 5 * i);
            qreal value = axis.bv + (axis.av - axis.bv) / 5 * i;
            (this->*func)(painter, fm, keDu, value);
        }
        break;
    case B_IS_ZERO:
        for (int i = 0; i <= 4; i++)
        {
            QPointF keDu(axis.a + (axis.b - axis.a) / 5 * i);
            qreal value = axis.av + (axis.bv - axis.av) / 5 * i;
            (this->*func)(painter, fm, keDu, value);
        }
        break;
    case CROSSED:
        huaJiaoChaKeDu(painter, fm, axis, func);
        break;
    }
}

void MPolyline::huaJiaoChaKeDu(QPainter* painter, const QFontMetrics& fm, const AxisInfo& axis, PaintFunc func)
{
    qreal step = calcBaseNumber(qMin(qAbs(axis.av), qAbs(axis.bv)));
    int ac = qRound(axis.av / step);
    int bc = qRound(axis.bv / step);
    int div = calcDivision(-ac, bc); /* ac为负,转为正 */
    if (div != -ac || div != bc)
    {
        ac /= div;
        bc /= div;
        step *= div;
    }
    else
    {
        step = axis.bv / 2;
        ac = -2;
        bc = 2;
    }
    QPointF offset = (axis.b - axis.a) / (bc - ac);
    for (int i = 0; i > ac; i--)
    {
        (this->*func)(painter, fm, axis.a - offset * i, axis.av - step * i);
    }
    for (int i = 0; i < bc; i++)
    {
        (this->*func)(painter, fm, axis.b - offset * i, axis.bv - step * i);
    }
}

//---------------------------------------------------------------------------------------
// 求两个正数的最大公约数
//---------------------------------------------------------------------------------------
int MPolyline::calcDivision(int a, int b)
{
    if (a < b)
    {
        std::swap(a, b);
    }
    while (a % b != 0)
    {
        int temp = a % b;
        a = b;
        b = temp;
    }
    return b;
}

void MPolyline::paintPolyline(QPainter* painter, const AxisInfo& xAxis, const AxisInfo& yAxis, int i)
{
    QPolygonF uiPts;
    for (auto it : datas[i])
    {
        qreal px = xAxis.a.x() + (xAxis.b.x() - xAxis.a.x()) * (it.x() - xAxis.av) / (xAxis.bv - xAxis.av);
        qreal py = yAxis.a.y() + (yAxis.b.y() - yAxis.a.y()) * (it.y() - yAxis.av) / (yAxis.bv - yAxis.av);
        uiPts.push_back(QPointF(px, py));
    }
    painter->setPen(QPen(QColor(colors[i]), 2));
    painter->drawPolyline(uiPts);
    painter->setPen(QPen(QColor(colors[i]), 5));
    painter->drawPoints(uiPts);
}

void MPolyline::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    QFontMetrics fm = painter.fontMetrics();
    painter.setBrush(Qt::NoBrush);
    painter.setPen(Qt::black);
    int ox, oy;
    AxisInfo xAxis, yAxis;
    calcAxisXInfo(xAxis, ox);
    calcAxisYInfo(yAxis, oy);
    xAxis.a.setY(oy);
    xAxis.b.setY(oy);
    yAxis.a.setX(ox);
    yAxis.b.setX(ox);

    /* 绘制原点 */
    QPoint axiso(ox, oy);
    QPoint ocenter(axiso.x() - 7, axiso.y() + 7);
    QSize osz = fm.size(0, u8"O");
    painter.drawText(QRect(QPoint(ocenter.x() - osz.width() / 2, ocenter.y() - osz.height() / 2), osz), u8"O");

    int xAxisStart = axism.left();
    int xAxisEnd = width() - axism.right();
    int yAxisStart = height() - axism.bottom();
    int yAxisEnd = axism.top();

    /* 绘制X轴 */
    QPoint axisx1(xAxisStart, oy);
    QPoint axisx2(xAxisEnd, oy);
    painter.drawLine(axisx1, axisx2);
    QPoint jianTouX[] = /* 向右箭头 */
    {
        { -5, 3 },
        { 0, 0 },
        { -5, -3 },
    };
    painter.save();
    painter.translate(axisx2);
    painter.drawPolyline(jianTouX, 3);
    painter.restore();
    huaKeDu(&painter, fm, xAxis, &MPolyline::huaKeDuX);

    /* 绘制Y轴 */
    QPoint axisy1(ox, yAxisStart);
    QPoint axisy2(ox, yAxisEnd);
    painter.drawLine(axisy1, axisy2);
    QPoint jianTouY[] = /* 向上箭头 */
    {
        { 3, 5 },
        { 0, 0 },
        { -3,5 },
    };
    painter.save();
    painter.translate(axisy2);
    painter.drawPolyline(jianTouY, 3);
    painter.restore();
    huaKeDu(&painter, fm, yAxis, &MPolyline::huaKeDuY);

    /* 绘制折线 */
    int count = (int)datas.size();
    for (int i = 0; i < count; i++)
    {
        paintPolyline(&painter, xAxis, yAxis, i);
    }
}

 

posted @ 2022-11-11 21:55  兜尼完  阅读(358)  评论(0编辑  收藏  举报