自定义的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); } }