自定义的Qt统计折线图控件

本控件用来显示一组统计数据,数据必须全为正,以折线图形式显示。可以添加折线颜色样式说明(即下图图表上方的甲公司、乙公司和丙公司的标识),参见文章“自定义的Qt给统计图添加颜色样例控件”。本控件在VS2015和Qt5.9上简单测试通过。控件使用方法是:创建类实例,先调用append函数添加数据,然后setDataTag设置添加数据每个数据的说明(即X轴需要绘制的文本)。目前必须先添加完数据才能调用setDataTag函数,否则会崩溃。下面是效果图:

下面上代码。我为了节省时间,有些代码是从我之前的文章中附的代码复制的(~~偷笑~~)。头文件:

class MLineChart : public QWidget
{
    Q_OBJECT

public:
    MLineChart(QWidget* parent = nullptr);
    QRgb append(const QVector<qreal>& idata);
    void setDataTag(int i, const QString& tagName);

private:
    struct AxisInfo;
    void paintEvent(QPaintEvent *event) override;
    QRgb obtainColor();
    qreal calcBaseNumber(qreal v);
    qreal calcNearNumber(qreal value, bool upperOrLower);
    void correctApex(qreal& min, qreal& max);
    void huaKeDuX(QPainter* painter, const QFontMetrics& fm, const QPoint& axiso, const QPoint& axisx, QVector<qreal>& xpoints);
    void huaKeDuY(QPainter* painter, const QFontMetrics& fm, const QPoint& axiso, const QPoint& axisy, AxisInfo& yInfo);
    std::pair<qreal, qreal> minMaxValue(const QVector<QVector<qreal>>& datas);

    struct AxisInfo
    {
        qreal minv;
        qreal maxv;
        qreal scrStart; /* 对应于minv的屏幕坐标 */
        qreal scrEnd; /* 对应于maxv的屏幕坐标 */
    };

private:
    const static QMargins margin;
    const static qreal maxRatio;
    const static qreal minRatio;
    QVector<QVector<qreal>> datas;
    QVector<QRgb> colors;
    QStringList tags;
};

CPP文件:

const QMargins MLineChart::margin(40, 20, 20, 25);
const qreal MLineChart::maxRatio = 0.9;
const qreal MLineChart::minRatio = 0.1;

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

QRgb MLineChart::append(const QVector<qreal>& idata)
{
    if (datas.empty())
    {
        datas.push_back(idata);
        int count = (int)idata.size();
        for (int i = 0; i < count; i++)
        {
            tags.append(QString(u8"第%1组").arg(i + 1));
        }
    }
    else
    {
        int dcount = (int)datas.first().size();
        int icount = (int)idata.size();
        if (dcount > icount)
        {
            QVector<qreal> temp = idata;
            icount = dcount - icount;
            for (int i = 0; i < icount; i++)
            {
                temp.push_back(0);
            }
            datas.push_back(qMove(temp));
        }
        else if (dcount < icount)
        {
            QVector<qreal> temp = idata;
            temp.erase(temp.begin() + dcount, temp.end());
            datas.push_back(qMove(temp));
        }
        else
        {
            datas.push_back(idata);
        }
    }
    QRgb rgb = obtainColor();
    colors.append(rgb);
    update();
    return rgb;
}

void MLineChart::setDataTag(int i, const QString& tagName)
{
    tags[i] = tagName;
    update();
}

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

std::pair<qreal, qreal> MLineChart::minMaxValue(const QVector<QVector<qreal>>& datas)
{
    std::pair<qreal, qreal> mmValue(0, 0);
    if (datas.empty())
    {
        return mmValue;
    }
    QVector<qreal> minmaxs;
    for (auto& it : datas)
    {
        auto apex = std::minmax_element(it.begin(), it.end());
        minmaxs.push_back(*apex.first);
        minmaxs.push_back(*apex.second);
    }
    auto apex = std::minmax_element(minmaxs.begin(), minmaxs.end());
    mmValue.first = *apex.first;
    mmValue.second = *apex.second;
    return mmValue;
}

void MLineChart::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;
    }
}

qreal MLineChart::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 MLineChart::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 MLineChart::huaKeDuX(QPainter* painter, const QFontMetrics& fm, const QPoint& axiso, const QPoint& axisx, QVector<qreal>& xpoints)
{
    int count = tags.size();
    qreal xMinLength = qAbs(axisx.x() - axiso.x()) * minRatio;
    qreal xMaxLength = qAbs(axisx.x() - axiso.x()) * maxRatio;
    qreal step = (xMaxLength - xMinLength) / (count - 1);
    /* 计算X轴刻度坐标 */
    for (int i = 0; i < count; i++)
    {
        qreal xv = axiso.x() + xMinLength + step * i;
        xpoints.push_back(xv);
    }
    /* 画X轴信息字符串 */
    const int tagExtend = 14;
    for (int i = 0; i < count; i++)
    {
        QPointF x1(xpoints[i], axiso.y() - 5);
        QPointF x2(xpoints[i], axiso.y());
        painter->drawLine(x1, x2);
        QPoint xcenter((int)xpoints[i], axiso.y() + tagExtend);
        QSize xsz = fm.size(0, tags[i]);
        painter->drawText(QRect(QPoint(xcenter.x() - xsz.width() / 2, xcenter.y() - xsz.height() / 2), xsz), tags[i]);
    }
}

void MLineChart::huaKeDuY(QPainter* painter, const QFontMetrics& fm, const QPoint& axiso, const QPoint& axisy, AxisInfo& yInfo)
{
    auto minmax = minMaxValue(datas);
    qreal minv = calcNearNumber(minmax.first, false);
    qreal maxv = calcNearNumber(minmax.second, true);
    correctApex(minv, maxv);
    /* 绘制Y轴上的刻度和对应的数字,只画5个刻度 */
    qreal yMaxLength = qAbs(axisy.y() - axiso.y()) * maxRatio;
    qreal yMinLength = qAbs(axisy.y() - axiso.y()) * minRatio;
    int loopCount = 5;
    if (minv == 0) /* 如数据最小是0,则扩展到原点分为5份 */
    {
        yMinLength = 0;
        loopCount = 4;
    }
    qreal ymax = axiso.y() - yMaxLength;
    qreal ymin = axiso.y() - yMinLength;
    for (int i = 0; i <= loopCount; i++)
    {
        qreal y = ymax + (ymin - ymax) / 5.0 * i;
        QPointF keDuY1(axiso.x(), y);
        QPointF keDuY2(axiso.x() + 5.0, y);
        painter->drawLine(keDuY1, keDuY2);
        QString zhi = QString::number(maxv + (minv - maxv) / 5 * i);
        QPoint zcenter(axiso.x() - margin.left() / 2, y);
        QSize zsz = fm.size(0, zhi);
        painter->drawText(QRect(QPoint(zcenter.x() - zsz.width() / 2, zcenter.y() - zsz.height() / 2), zsz), zhi);
    }
    yInfo.minv = minv;
    yInfo.maxv = maxv;
    yInfo.scrStart = ymin;
    yInfo.scrEnd = ymax;
}

void MLineChart::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    QFontMetrics fm = painter.fontMetrics();
    painter.setBrush(Qt::NoBrush);
    painter.setPen(Qt::black);

    /* 绘制原点 */
    QPoint axiso(margin.left(), height() - margin.bottom());
    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");

    /* 绘制X轴 */
    QPoint axisx(width() - margin.right(), height() - margin.bottom());
    painter.drawLine(axiso, axisx);
    QPoint jianTouX[] = /* 向右箭头 */
    {
        { -5, 3 },
        { 0, 0 },
        { -5, -3 },
    };
    painter.save();
    painter.translate(axisx);
    painter.drawPolyline(jianTouX, 3);
    painter.restore();
    /* 计算X坐标刻度位置 */
    QVector<qreal> xpoints;
    huaKeDuX(&painter, fm, axiso, axisx, xpoints);

    /* 绘制Y轴 */
    QPoint axisy(margin.left(), margin.top());
    painter.drawLine(axiso, axisy);
    QPoint jianTouY[] = /* 向上箭头 */
    {
        { 3, 5 },
        { 0, 0 },
        { -3, 5 },
    };
    painter.save();
    painter.translate(axisy);;
    painter.drawPolyline(jianTouY, 3);
    painter.restore();
    /* 计算Y坐标刻度位置 */
    AxisInfo yInfo;
    huaKeDuY(&painter, fm, axiso, axisy, yInfo);

    /* 画数据 */
    int dcount = datas.size();
    for (int i = 0; i < dcount; i++)
    {
        const QVector<qreal>& pts = datas[i];
        int pcount = pts.size();
        QPolygonF poly;
        for (int j = 0; j < pcount; j++)
        {
            qreal y = yInfo.scrStart + (yInfo.scrEnd - yInfo.scrStart) * (pts[j] - yInfo.minv) / (yInfo.maxv - yInfo.minv);
            poly.append(QPointF(xpoints[j], y));
        }
        painter.setPen(QPen(QColor(colors[i]), 2));
        painter.drawPolyline(poly);
        painter.setPen(QPen(QColor(colors[i]), 5));
        painter.drawPoints(poly);
    }
}

 

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