Browse Source

添加五种绘图

Signed-off-by: codeClown <zhaomengshou@126.com>
codeClown 1 year ago
parent
commit
c5344a601e

+ 6 - 0
QFD/QFD.pro

@@ -79,6 +79,7 @@ SOURCES += \
     view/HomeView.cpp \
     view/LoginView.cpp \
     view/MainWindow.cpp \
+    view/PlotView.cpp \
     view/ProjectView.cpp \
     view/SettingView.cpp \
     view/UserView.cpp \
@@ -86,6 +87,8 @@ SOURCES += \
     widgets/AppInfoWidget.cpp \
     widgets/ConfigExpertWidget.cpp \
     widgets/CreateProjWidget.cpp \
+    widgets/CustomPie.cpp \
+    widgets/CustomPieChart.cpp \
     widgets/CustomTitleBar.cpp \
     widgets/DataCollectionWidget.cpp \
     widgets/DataProcessingWidget.cpp \
@@ -122,6 +125,7 @@ HEADERS += \
     view/HomeView.h \
     view/LoginView.h \
     view/MainWindow.h \
+    view/PlotView.h \
     view/ProjectView.h \
     view/SettingView.h \
     view/UserView.h \
@@ -129,6 +133,8 @@ HEADERS += \
     widgets/AppInfoWidget.h \
     widgets/ConfigExpertWidget.h \
     widgets/CreateProjWidget.h \
+    widgets/CustomPie.h \
+    widgets/CustomPieChart.h \
     widgets/CustomTitleBar.h \
     widgets/DataCollectionWidget.h \
     widgets/DataProcessingWidget.h \

+ 2 - 1
QFD/main.cpp

@@ -15,6 +15,8 @@ int main(int argc, char *argv[])
     a.setApplicationName("QFD2");
     a.setApplicationDisplayName("防护评估系统");
 
+    Q_INIT_RESOURCE(qfluentwidgets);
+
     //    LoginView l;
     //    l.show();
 
@@ -25,7 +27,6 @@ int main(int argc, char *argv[])
     MainWindow m;
     m.show();
     qDebug() << QSqlDatabase::drivers();
-    Q_INIT_RESOURCE(qfluentwidgets);
 
     return a.exec();
 }

+ 99 - 0
QFD/qcustomplot/CustomHistogramBars.cpp

@@ -0,0 +1,99 @@
+#include "CustomHistogramBars.h"
+
+CustomHistogramBars::CustomHistogramBars(QCPAxis *keyAxis, QCPAxis *valueAxis)
+    : QCPBars(keyAxis, valueAxis),
+      m_textAlignment(Qt::AlignCenter),
+      m_spacing(5),
+      m_font(QFont(QLatin1String("sans serif"), 12))
+{
+}
+
+void CustomHistogramBars::setTextAlignment(Qt::Alignment alignment)
+{
+    m_textAlignment = alignment;
+}
+
+void CustomHistogramBars::setSpacing(double spacing)
+{
+    m_spacing = spacing;
+}
+
+void CustomHistogramBars::setFont(const QFont &font)
+{
+    m_font = font;
+}
+
+void CustomHistogramBars::draw(QCPPainter *painter)
+{
+    if (!mKeyAxis || !mValueAxis) {
+        qDebug() << Q_FUNC_INFO << "invalid key or value axis";
+        return;
+    }
+    if (mDataContainer->isEmpty())
+        return;
+
+    QCPBarsDataContainer::const_iterator visibleBegin, visibleEnd;
+    getVisibleDataBounds(visibleBegin, visibleEnd);
+
+    // loop over and draw segments of unselected/selected data:
+    QList<QCPDataRange> selectedSegments, unselectedSegments, allSegments;
+    getDataSegments(selectedSegments, unselectedSegments);
+    allSegments << unselectedSegments << selectedSegments;
+    for (int i = 0; i < allSegments.size(); ++i) {
+        bool isSelectedSegment                     = i >= unselectedSegments.size();
+        QCPBarsDataContainer::const_iterator begin = visibleBegin;
+        QCPBarsDataContainer::const_iterator end   = visibleEnd;
+        mDataContainer->limitIteratorsToDataRange(begin, end, allSegments.at(i));
+        if (begin == end)
+            continue;
+
+        for (QCPBarsDataContainer::const_iterator it = begin; it != end; ++it) {
+            // check data validity if flag set:
+#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
+            // draw bar:
+            if (isSelectedSegment && mSelectionDecorator) {
+                mSelectionDecorator->applyBrush(painter);
+                mSelectionDecorator->applyPen(painter);
+            } else {
+                painter->setBrush(mBrush);
+                painter->setPen(mPen);
+            }
+            applyDefaultAntialiasingHint(painter);
+
+            // 修改一下
+            QRectF barRect = getBarRect(it->key, it->value);
+            painter->drawPolygon(barRect);
+
+            // 计算文字的位置
+            painter->setFont(m_font);                           // 设置字体
+            QString text = QString::number(it->value, 'g', 2);  // 取得当前value轴的值,保留两位精度
+
+            QRectF textRect = painter->fontMetrics().boundingRect(0, 0, 0, 0, Qt::TextDontClip | m_textAlignment,
+                                                                  text);  // 计算文字所占用的大小
+
+            if (mKeyAxis.data()->orientation() == Qt::Horizontal) {  // 当key轴为水平轴的时候
+                if (mKeyAxis.data()->axisType() == QCPAxis::atTop)   // 上轴,移动文字到柱状图下面
+                    textRect.moveTopLeft(barRect.bottomLeft() + QPointF(0, m_spacing));
+                else  // 下轴,移动文字到柱状图上面
+                    textRect.moveBottomLeft(barRect.topLeft() - QPointF(0, m_spacing));
+                textRect.setWidth(barRect.width());
+                painter->drawText(textRect, Qt::TextDontClip | m_textAlignment, text);
+            } else {                                                 // 当key轴为竖直轴的时候
+                if (mKeyAxis.data()->axisType() == QCPAxis::atLeft)  // 左轴,移动文字到柱状图右边
+                    textRect.moveTopLeft(barRect.topRight() + QPointF(m_spacing, 0));
+                else  // 右轴,移动文字到柱状图左边
+                    textRect.moveTopRight(barRect.topLeft() - QPointF(m_spacing, 0));
+                textRect.setHeight(barRect.height());
+                painter->drawText(textRect, Qt::TextDontClip | m_textAlignment, text);
+            }
+        }
+    }
+
+    // draw other selection decoration that isn't just line/scatter pens and brushes:
+    if (mSelectionDecorator)
+        mSelectionDecorator->drawDecoration(painter, selection());
+}

+ 32 - 0
QFD/qcustomplot/CustomHistogramBars.h

@@ -0,0 +1,32 @@
+#ifndef CUSTOMHISTOGRAMBARS_H
+#define CUSTOMHISTOGRAMBARS_H
+
+#include "qcustomplot.h"
+
+/**
+ * @brief 柱状图上显示值
+ */
+class CustomHistogramBars : public QCPBars
+{
+    Q_OBJECT
+
+public:
+    explicit CustomHistogramBars(QCPAxis *keyAxis, QCPAxis *valueAxis);
+
+    Qt::Alignment textAligment() const { return m_textAlignment; }
+    double spacing() const { return m_spacing; }
+    QFont font() const { return m_font; }
+
+    void setTextAlignment(Qt::Alignment alignment);
+    void setSpacing(double spacing);
+    void setFont(const QFont &font);
+
+protected:
+    Qt::Alignment m_textAlignment;  // 文字对齐方式
+    double m_spacing;               // 文字与柱状图的间距,这里按像素大小
+    QFont m_font;                   // 文字使用的字体
+
+    virtual void draw(QCPPainter *painter) Q_DECL_OVERRIDE;
+};
+
+#endif  // CUSTOMHISTOGRAMBARS_H

+ 190 - 0
QFD/qcustomplot/SmoothCurveGraph.cpp

@@ -0,0 +1,190 @@
+#include "SmoothCurveGraph.h"
+
+#include <QPointF>
+#include <QVector>
+
+class SmoothCurveGenerator
+{
+protected:
+    static QPainterPath generateSmoothCurveImp(const QVector<QPointF> &points)
+    {
+        QPainterPath path;
+        int len = points.size();
+
+        if (len < 2) {
+            return path;
+        }
+
+        QVector<QPointF> firstControlPoints;
+        QVector<QPointF> secondControlPoints;
+        calculateControlPoints(points, &firstControlPoints, &secondControlPoints);
+
+        path.moveTo(points[0].x(), points[0].y());
+
+        // Using bezier curve to generate a smooth curve.
+        for (int i = 0; i < len - 1; ++i) {
+            path.cubicTo(firstControlPoints[i], secondControlPoints[i], points[i + 1]);
+        }
+
+        return path;
+    }
+
+public:
+    static QPainterPath generateSmoothCurve(const QVector<QPointF> &points)
+    {
+        QPainterPath result;
+
+        int segmentStart = 0;
+        int i            = 0;
+        int pointSize    = points.size();
+        while (i < pointSize) {
+            if (qIsNaN(points.at(i).y()) || qIsNaN(points.at(i).x()) || qIsInf(points.at(i).y())) {
+                QVector<QPointF> lineData;  // QVector<QPointF>(points.constBegin() + segmentStart, points.constBegin()
+                                            // + i - segmentStart);
+                for (int s = segmentStart; s < i - segmentStart; ++s) {
+                    lineData << points.at(s);
+                }
+
+                result.addPath(generateSmoothCurveImp(lineData));
+                segmentStart = i + 1;
+            }
+            ++i;
+        }
+        QVector<QPointF> lineData;  //(QVector<QPointF>(points.constBegin() + segmentStart, points.constEnd()));
+        for (int s = segmentStart; s < points.size(); ++s) {
+            lineData << points.at(s);
+        }
+        result.addPath(generateSmoothCurveImp(lineData));
+        return result;
+    }
+
+    static QPainterPath generateSmoothCurve(const QPainterPath &basePath, const QVector<QPointF> &points)
+    {
+        if (points.isEmpty())
+            return basePath;
+
+        QPainterPath path = basePath;
+        int len           = points.size();
+        if (len == 1) {
+            path.lineTo(points.at(0));
+            return path;
+        }
+
+        QVector<QPointF> firstControlPoints;
+        QVector<QPointF> secondControlPoints;
+        calculateControlPoints(points, &firstControlPoints, &secondControlPoints);
+
+        path.lineTo(points.at(0));
+        for (int i = 0; i < len - 1; ++i)
+            path.cubicTo(firstControlPoints[i], secondControlPoints[i], points[i + 1]);
+
+        return path;
+    }
+
+    static void calculateFirstControlPoints(double *&result, const double *rhs, int n)
+    {
+        result      = new double[n];
+        double *tmp = new double[n];
+        double b    = 2.0;
+        result[0]   = rhs[0] / b;
+
+        // Decomposition and forward substitution.
+        for (int i = 1; i < n; i++) {
+            tmp[i]    = 1 / b;
+            b         = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
+            result[i] = (rhs[i] - result[i - 1]) / b;
+        }
+
+        for (int i = 1; i < n; i++) {
+            result[n - i - 1] -= tmp[n - i] * result[n - i];  // Backsubstitution.
+        }
+
+        delete[] tmp;
+    }
+
+    static void calculateControlPoints(const QVector<QPointF> &knots, QVector<QPointF> *firstControlPoints,
+                                       QVector<QPointF> *secondControlPoints)
+    {
+        int n = knots.size() - 1;
+
+        firstControlPoints->reserve(n);
+        secondControlPoints->reserve(n);
+
+        for (int i = 0; i < n; ++i) {
+            firstControlPoints->append(QPointF());
+            secondControlPoints->append(QPointF());
+        }
+
+        if (n == 1) {
+            // Special case: Bezier curve should be a straight line.
+            // P1 = (2P0 + P3) / 3
+            (*firstControlPoints)[0].rx() = (2 * knots[0].x() + knots[1].x()) / 3;
+            (*firstControlPoints)[0].ry() = (2 * knots[0].y() + knots[1].y()) / 3;
+
+            // P2 = 2P1 – P0
+            (*secondControlPoints)[0].rx() = 2 * (*firstControlPoints)[0].x() - knots[0].x();
+            (*secondControlPoints)[0].ry() = 2 * (*firstControlPoints)[0].y() - knots[0].y();
+
+            return;
+        }
+
+        // Calculate first Bezier control points
+        double *xs   = nullptr;
+        double *ys   = nullptr;
+        double *rhsx = new double[n];  // Right hand side vector
+        double *rhsy = new double[n];  // Right hand side vector
+
+        // Set right hand side values
+        for (int i = 1; i < n - 1; ++i) {
+            rhsx[i] = 4 * knots[i].x() + 2 * knots[i + 1].x();
+            rhsy[i] = 4 * knots[i].y() + 2 * knots[i + 1].y();
+        }
+        rhsx[0]     = knots[0].x() + 2 * knots[1].x();
+        rhsx[n - 1] = (8 * knots[n - 1].x() + knots[n].x()) / 2.0;
+        rhsy[0]     = knots[0].y() + 2 * knots[1].y();
+        rhsy[n - 1] = (8 * knots[n - 1].y() + knots[n].y()) / 2.0;
+
+        // Calculate first control points coordinates
+        calculateFirstControlPoints(xs, rhsx, n);
+        calculateFirstControlPoints(ys, rhsy, n);
+
+        // Fill output control points.
+        for (int i = 0; i < n; ++i) {
+            (*firstControlPoints)[i].rx() = xs[i];
+            (*firstControlPoints)[i].ry() = ys[i];
+
+            if (i < n - 1) {
+                (*secondControlPoints)[i].rx() = 2 * knots[i + 1].x() - xs[i + 1];
+                (*secondControlPoints)[i].ry() = 2 * knots[i + 1].y() - ys[i + 1];
+            } else {
+                (*secondControlPoints)[i].rx() = (knots[n].x() + xs[n - 1]) / 2;
+                (*secondControlPoints)[i].ry() = (knots[n].y() + ys[n - 1]) / 2;
+            }
+        }
+
+        delete xs;
+        delete ys;
+        delete[] rhsx;
+        delete[] rhsy;
+    }
+};
+
+SmoothCurveGraph::SmoothCurveGraph(QCPAxis *keyAxis, QCPAxis *valueAxis) : QCPGraph(keyAxis, valueAxis), m_smooth(true)
+{
+}
+
+void SmoothCurveGraph::setSmooth(bool smooth)
+{
+    m_smooth = smooth;
+}
+
+void SmoothCurveGraph::drawLinePlot(QCPPainter *painter, const QVector<QPointF> &lines) const
+{
+    if (painter->pen().style() != Qt::NoPen && painter->pen().color().alpha() != 0) {
+        applyDefaultAntialiasingHint(painter);
+        if (m_smooth && mLineStyle == lsLine)
+            painter->drawPath(SmoothCurveGenerator::generateSmoothCurve(lines));
+        else
+            drawPolyline(painter, lines);
+    }
+}

+ 23 - 0
QFD/qcustomplot/SmoothCurveGraph.h

@@ -0,0 +1,23 @@
+#ifndef SMOOTHCURVEGRAPH_H
+#define SMOOTHCURVEGRAPH_H
+
+#include "qcustomplot.h"
+
+class SmoothCurveGraph : public QCPGraph
+{
+    Q_OBJECT
+
+public:
+    SmoothCurveGraph(QCPAxis *keyAxis, QCPAxis *valueAxis);
+
+    void setSmooth(bool smooth);
+
+protected:
+    bool m_smooth;
+
+    // QCPGraph interface
+protected:
+    virtual void drawLinePlot(QCPPainter *painter, const QVector<QPointF> &lines) const override;
+};
+
+#endif  // SMOOTHCURVEGRAPH_H

+ 82 - 0
QFD/qcustomplot/axis_tag.cpp

@@ -0,0 +1,82 @@
+#include "axis_tag.h"
+
+AxisTag::AxisTag(QCPAxis *parentAxis) : QObject(parentAxis), mAxis(parentAxis)
+{
+    // The dummy tracer serves here as an invisible anchor which always sticks to the right side of
+    // the axis rect
+    mDummyTracer = new QCPItemTracer(mAxis->parentPlot());
+    mDummyTracer->setVisible(false);
+    mDummyTracer->position->setTypeX(QCPItemPosition::ptAxisRectRatio);
+    mDummyTracer->position->setTypeY(QCPItemPosition::ptPlotCoords);
+    mDummyTracer->position->setAxisRect(mAxis->axisRect());
+    mDummyTracer->position->setAxes(0, mAxis);
+    mDummyTracer->position->setCoords(1, 0);
+
+    // the arrow end (head) is set to move along with the dummy tracer by setting it as its parent
+    // anchor. Its coordinate system (setCoords) is thus pixels, and this is how the needed horizontal
+    // offset for the tag of the second y axis is achieved. This horizontal offset gets dynamically
+    // updated in AxisTag::updatePosition. the arrow "start" is simply set to have the "end" as parent
+    // anchor. It is given a horizontal offset to the right, which results in a 15 pixel long arrow.
+    mArrow = new QCPItemLine(mAxis->parentPlot());
+    mArrow->setLayer("overlay");
+    mArrow->setClipToAxisRect(false);
+    mArrow->setHead(QCPLineEnding::esSpikeArrow);
+    mArrow->end->setParentAnchor(mDummyTracer->position);
+    mArrow->start->setParentAnchor(mArrow->end);
+    mArrow->start->setCoords(15, 0);
+
+    // The text label is anchored at the arrow start (tail) and has its "position" aligned at the
+    // left, and vertically centered to the text label box.
+    mLabel = new QCPItemText(mAxis->parentPlot());
+    mLabel->setLayer("overlay");
+    mLabel->setClipToAxisRect(false);
+    mLabel->setPadding(QMargins(3, 0, 3, 0));
+    mLabel->setBrush(QBrush(Qt::white));
+    mLabel->setPen(QPen(Qt::blue));
+    mLabel->setPositionAlignment(Qt::AlignLeft | Qt::AlignVCenter);
+    mLabel->position->setParentAnchor(mArrow->start);
+}
+
+AxisTag::~AxisTag()
+{
+    if (mDummyTracer)
+        mDummyTracer->parentPlot()->removeItem(mDummyTracer);
+    if (mArrow)
+        mArrow->parentPlot()->removeItem(mArrow);
+    if (mLabel)
+        mLabel->parentPlot()->removeItem(mLabel);
+}
+
+void AxisTag::setPen(const QPen &pen)
+{
+    mArrow->setPen(pen);
+    mLabel->setPen(pen);
+}
+
+void AxisTag::setBrush(const QBrush &brush)
+{
+    mLabel->setBrush(brush);
+}
+
+void AxisTag::setText(const QString &text)
+{
+    mLabel->setText(text);
+}
+
+void AxisTag::updatePosition(double value)
+{
+    // since both the arrow and the text label are chained to the dummy tracer (via anchor
+    // parent-child relationships) it is sufficient to update the dummy tracer coordinates. The
+    // Horizontal coordinate type was set to ptAxisRectRatio so to keep it aligned at the right side
+    // of the axis rect, it is always kept at 1. The vertical coordinate type was set to
+    // ptPlotCoordinates of the passed parent axis, so the vertical coordinate is set to the new
+    // value.
+    mDummyTracer->position->setCoords(1, value);
+
+    // We want the arrow head to be at the same horizontal position as the axis backbone, even if
+    // the axis has a certain offset from the axis rect border (like the added second y axis). Thus we
+    // set the horizontal pixel position of the arrow end (head) to the axis offset (the pixel
+    // distance to the axis rect border). This works because the parent anchor of the arrow end is
+    // the dummy tracer, which, as described earlier, is tied to the right axis rect border.
+    mArrow->end->setCoords(mAxis->offset(), 0);
+}

+ 34 - 0
QFD/qcustomplot/axis_tag.h

@@ -0,0 +1,34 @@
+#ifndef AXIS_TAG_H
+#define AXIS_TAG_H
+
+#include <QObject>
+#include "qcustomplot.h"
+
+class AxisTag : public QObject
+{
+    Q_OBJECT
+public:
+    explicit AxisTag(QCPAxis *parentAxis);
+    virtual ~AxisTag();
+
+    // setters:
+    void setPen(const QPen &pen);
+    void setBrush(const QBrush &brush);
+    void setText(const QString &text);
+
+    // getters:
+    QPen pen() const { return mLabel->pen(); }
+    QBrush brush() const { return mLabel->brush(); }
+    QString text() const { return mLabel->text(); }
+
+    // other methods:
+    void updatePosition(double value);
+
+protected:
+    QCPAxis *mAxis;
+    QPointer<QCPItemTracer> mDummyTracer;
+    QPointer<QCPItemLine> mArrow;
+    QPointer<QCPItemText> mLabel;
+};
+
+#endif  // AXIS_TAG_H

+ 6 - 0
QFD/qcustomplot/qcustomplot.pri

@@ -2,11 +2,17 @@ QT += core gui
 greaterThan(QT_MAJOR_VERSION, 4): QT += widgets printsupport
 
 SOURCES += $$PWD/qcustomplot.cpp \
+        $$PWD/CustomHistogramBars.cpp \
+        $$PWD/SmoothCurveGraph.cpp \
         $$PWD/axis_rect.cpp \
+        $$PWD/axis_tag.cpp \
         $$PWD/graph.cpp \
         $$PWD/graph_tracer.cpp
 		
 HEADERS += $$PWD/qcustomplot.h \
+        $$PWD/CustomHistogramBars.h \
+        $$PWD/SmoothCurveGraph.h \
         $$PWD/axis_rect.h \
+        $$PWD/axis_tag.h \
         $$PWD/graph.h \
         $$PWD/graph_tracer.h

+ 294 - 0
QFD/view/PlotView.cpp

@@ -0,0 +1,294 @@
+#include "PlotView.h"
+
+#include "qcustomplot/CustomHistogramBars.h"
+#include "qcustomplot/SmoothCurveGraph.h"
+
+PlotView::PlotView(PlotType t, const QVector<Data> &data, const QString &title, QWidget *parent)
+    : QCustomPlot(parent), m_plotType(t), m_sourceData(data), m_title(title)
+{
+}
+
+void PlotView::updateType(PlotView::PlotType t)
+{
+    m_plotType = t;
+    plot();
+}
+
+void PlotView::updateData(const QVector<PlotView::Data> &data)
+{
+    m_sourceData = data;
+    plot();
+}
+
+QPixmap PlotView::toPixmap()
+{
+    QPixmap pixmap(this->size());
+    pixmap.fill(Qt::transparent);
+    this->render(&pixmap);
+    return pixmap;
+}
+
+void PlotView::plot()
+{
+    this->clearGraphs();
+    switch (m_plotType) {
+    case Line:
+        drawLine();
+        break;
+    case HistogramHorizontal:
+        drawHistogram(true);
+        break;
+
+    case HistogramVertical:
+        drawHistogram(false);
+        break;
+
+    case Curve:
+        drawCurve();
+        break;
+
+    case Area:
+        drawArea();
+        break;
+
+    default:
+        break;
+    }
+}
+
+void PlotView::drawLine()
+{
+    this->legend->setVisible(true);
+    this->legend->setFont(QFont("微软雅黑", 9));
+    this->legend->setRowSpacing(-3);
+    QVector<QCPScatterStyle::ScatterShape> shapes;
+    shapes << QCPScatterStyle::ssDot;
+    shapes << QCPScatterStyle::ssCross;
+    shapes << QCPScatterStyle::ssPlus;
+    shapes << QCPScatterStyle::ssCircle;
+    shapes << QCPScatterStyle::ssDisc;
+    shapes << QCPScatterStyle::ssSquare;
+    shapes << QCPScatterStyle::ssDiamond;
+    shapes << QCPScatterStyle::ssStar;
+    shapes << QCPScatterStyle::ssTriangle;
+    shapes << QCPScatterStyle::ssTriangleInverted;
+    shapes << QCPScatterStyle::ssCrossSquare;
+    shapes << QCPScatterStyle::ssPlusSquare;
+    shapes << QCPScatterStyle::ssCrossCircle;
+    shapes << QCPScatterStyle::ssPlusCircle;
+    shapes << QCPScatterStyle::ssPeace;
+    shapes << QCPScatterStyle::ssCustom;
+
+    QPen pen;
+
+    for (int i = 0; i < m_sourceData.size(); i++) {
+        this->addGraph();
+        pen.setColor(
+                QColor(qSin(i * 0.3) * 100 + 100, qSin(i * 0.6 + 0.7) * 100 + 100, qSin(i * 0.4 + 0.6) * 100 + 100));
+        // generate data:
+        QVector<double> x, y;
+        x << 0 << 1;
+        y << 0 << m_sourceData.at(i).value;
+
+        this->graph()->setData(x, y);
+        this->graph()->rescaleAxes(true);
+        this->graph()->setPen(pen);
+        this->graph()->setName(m_sourceData.at(i).name);
+        this->graph()->setLineStyle(QCPGraph::lsLine);
+        // set scatter style:
+        if (shapes.at(i % shapes.size()) != QCPScatterStyle::ssCustom) {
+            this->graph()->setScatterStyle(QCPScatterStyle(shapes.at(i % shapes.size()), 10));
+        } else {
+            QPainterPath customScatterPath;
+            for (int i = 0; i < 3; ++i)
+                customScatterPath.cubicTo(qCos(2 * M_PI * i / 3.0) * 9, qSin(2 * M_PI * i / 3.0) * 9,
+                                          qCos(2 * M_PI * (i + 0.9) / 3.0) * 9, qSin(2 * M_PI * (i + 0.9) / 3.0) * 9, 0,
+                                          0);
+            this->graph()->setScatterStyle(
+                    QCPScatterStyle(customScatterPath, QPen(Qt::black, 0), QColor(40, 70, 255, 50), 10));
+        }
+    }
+
+    // set blank axis lines:
+    this->rescaleAxes();
+    this->xAxis->setTicks(false);
+    this->yAxis->setTicks(true);
+    this->xAxis->setTickLabels(false);
+    this->yAxis->setTickLabels(true);
+    if (!m_title.isEmpty()) {
+        this->xAxis->setLabel(m_title);
+    }
+    // set axis ranges to show all data:
+    this->xAxis->setRange(0, 1.2);
+    this->yAxis->setRange(0, 2);
+
+    // make top right axes clones of bottom left axes:
+    this->axisRect()->setupFullAxesBox();
+
+    // make left and bottom axes always transfer their ranges to right and top axes:
+    connect(this->xAxis, SIGNAL(rangeChanged(QCPRange)), this->xAxis2, SLOT(setRange(QCPRange)));
+    connect(this->yAxis, SIGNAL(rangeChanged(QCPRange)), this->yAxis2, SLOT(setRange(QCPRange)));
+
+    // Note: we could have also just called customPlot->rescaleAxes(); instead
+    // Allow user to drag axis ranges with mouse, zoom with mouse wheel and select graphs by clicking:
+    this->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
+
+    this->replot();
+}
+
+void PlotView::drawHistogram(bool horizontal)
+{
+    QCPAxis *keyAxis   = nullptr;
+    QCPAxis *valueAxis = nullptr;
+    if (horizontal) {
+        keyAxis   = this->yAxis;
+        valueAxis = this->xAxis;
+    } else {
+        keyAxis   = this->xAxis;
+        valueAxis = this->yAxis;
+    }
+    //    QCPBars *fossil    = new QCPBars(keyAxis, valueAxis);  // 使用xAxis作为柱状图的key轴,yAxis作为value轴
+    CustomHistogramBars *fossil = new CustomHistogramBars(keyAxis, valueAxis);
+
+    fossil->setAntialiased(false);                           // 为了更好的边框效果,关闭抗齿锯
+    fossil->setName("Fossil fuels");                         // 设置柱状图的名字,可在图例中显示
+    fossil->setPen(QPen(QColor(0, 168, 140).lighter(130)));  // 设置柱状图的边框颜色
+    fossil->setBrush(QColor(0, 168, 140));                   // 设置柱状图的画刷颜色
+
+    // 为柱状图设置一个文字类型的key轴,ticks决定了轴的范围,而labels决定了轴的刻度文字的显示
+    QVector<double> ticks;
+    QVector<QString> labels;
+    QVector<double> fossilData;
+    for (int i = 0; i < m_sourceData.size(); ++i) {
+        ticks << i + 1;
+        labels << m_sourceData.at(i).name;
+        fossilData << m_sourceData.at(i).value;
+    }
+    QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText);
+    textTicker->addTicks(ticks, labels);
+
+    keyAxis->setTicker(textTicker);  // 设置为文字轴
+
+    keyAxis->setTickLabelRotation(60);  // 轴刻度文字旋转60度
+    keyAxis->setSubTicks(false);        // 不显示子刻度
+    keyAxis->setTickLength(0, 4);       // 轴内外刻度的长度分别是0,4,也就是轴内的刻度线不显示
+    keyAxis->setRange(0, ticks.size() + 1);  // 设置范围
+    keyAxis->setUpperEnding(QCPLineEnding::esSpikeArrow);
+
+    if (!m_title.isEmpty()) {
+        keyAxis->setLabel(m_title);
+    }
+
+    valueAxis->setRange(0, 1.1);
+    valueAxis->setPadding(35);  // 轴的内边距,可以到QCustomPlot之开始(一)看图解
+    valueAxis->setUpperEnding(QCPLineEnding::esSpikeArrow);
+    fossil->setData(ticks, fossilData);
+
+    this->replot();
+}
+
+void PlotView::drawCurve()
+{
+    QCPAxis *keyAxis   = this->xAxis;
+    QCPAxis *valueAxis = this->yAxis;
+
+    SmoothCurveGraph *scGraph = new SmoothCurveGraph(keyAxis, valueAxis);
+
+    scGraph->setPen(QPen(Qt::blue));  // line color blue for first graph
+
+    QVector<double> x, y;
+    QVector<QString> labels;
+    for (int i = 0; i < m_sourceData.size(); ++i) {
+        x << i + 1;
+        y << m_sourceData.at(i).value;
+        labels << m_sourceData.at(i).name;
+    }
+    // configure right and top axis to show ticks but no labels:
+    // (see QCPAxisRect::setupFullAxesBox for a quicker method to do this)
+    this->xAxis2->setVisible(true);
+    this->xAxis2->setTickLabels(false);
+    this->yAxis2->setVisible(true);
+    this->yAxis2->setTickLabels(false);
+    // make left and bottom axes always transfer their ranges to right and top axes:
+    connect(this->xAxis, SIGNAL(rangeChanged(QCPRange)), this->xAxis2, SLOT(setRange(QCPRange)));
+    connect(this->yAxis, SIGNAL(rangeChanged(QCPRange)), this->yAxis2, SLOT(setRange(QCPRange)));
+    // pass data points to graphs:
+    scGraph->setData(x, y);
+    scGraph->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCross, QColor(Qt::red), QColor(Qt::white), 4));
+
+    QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText);
+    textTicker->addTicks(x, labels);
+
+    keyAxis->setTicker(textTicker);  // 设置为文字轴
+
+    keyAxis->setTickLabelRotation(60);   // 轴刻度文字旋转60度
+    keyAxis->setSubTicks(false);         // 不显示子刻度
+    keyAxis->setTickLength(0, 4);        // 轴内外刻度的长度分别是0,4,也就是轴内的刻度线不显示
+    keyAxis->setRange(0, x.size() + 1);  // 设置范围
+    keyAxis->setUpperEnding(QCPLineEnding::esSpikeArrow);
+
+    if (!m_title.isEmpty()) {
+        keyAxis->setLabel(m_title);
+    }
+
+    // let the ranges scale themselves so graph 0 fits perfectly in the visible area:
+    scGraph->rescaleAxes();
+    // Note: we could have also just called customPlot->rescaleAxes(); instead
+    // Allow user to drag axis ranges with mouse, zoom with mouse wheel and select graphs by clicking:
+    this->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
+
+    this->replot();
+}
+
+void PlotView::drawArea()
+{
+    // create multiple graphs:
+    for (int gi = 0; gi < m_sourceData.size(); ++gi) {
+        this->addGraph();
+        QColor color(20 + 200 / m_sourceData.size() * gi, 70 * (1.6 - gi / m_sourceData.size()), 150, 150);
+        this->graph()->setLineStyle(QCPGraph::lsLine);
+        this->graph()->setPen(QPen(color.lighter(200)));
+        this->graph()->setBrush(QBrush(color));
+        this->graph()->setName(m_sourceData.at(gi).name);
+        this->graph()->setScatterStyle(
+                QCPScatterStyle(QCPScatterStyle::ssCross, QColor(Qt::blue), QColor(Qt::white), 4));
+        // generate random walk data:
+        QVector<QCPGraphData> timeData(2);
+        timeData[0].key   = 0;
+        timeData[0].value = 0;
+        timeData[1].key   = 1;
+        timeData[1].value = m_sourceData.at(gi).value;
+        this->graph()->data()->set(timeData);
+    }
+
+    // set axis labels:
+    if (!m_title.isEmpty()) {
+        this->xAxis->setLabel(m_title);
+    }
+
+    this->xAxis->setTicks(false);
+    this->yAxis->setTicks(true);
+    this->xAxis->setTickLabels(false);
+    this->yAxis->setTickLabels(true);
+    // set axis ranges to show all data:
+    this->xAxis->setRange(0, 1.2);
+    this->yAxis->setRange(0, 2);
+
+    // make left and bottom axes always transfer their ranges to right and top axes:
+    connect(this->xAxis, SIGNAL(rangeChanged(QCPRange)), this->xAxis2, SLOT(setRange(QCPRange)));
+    connect(this->yAxis, SIGNAL(rangeChanged(QCPRange)), this->yAxis2, SLOT(setRange(QCPRange)));
+
+    // show legend with slightly transparent background brush:
+    this->legend->setVisible(true);
+    this->legend->setFont(QFont("微软雅黑", 9));
+    this->legend->setRowSpacing(-3);
+    this->legend->setBrush(QColor(255, 255, 255, 150));
+
+    // let the ranges scale themselves so graph 0 fits perfectly in the visible area:
+    //    this->rescaleAxes();
+    // Note: we could have also just called customPlot->rescaleAxes(); instead
+    // Allow user to drag axis ranges with mouse, zoom with mouse wheel and select graphs by clicking:
+    this->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
+
+    this->replot();
+}

+ 78 - 0
QFD/view/PlotView.h

@@ -0,0 +1,78 @@
+#ifndef PLOTVIEW_H
+#define PLOTVIEW_H
+
+#include <qcustomplot/qcustomplot.h>
+
+/**
+ * @例子
+ *   QVector<PlotView::Data> pd;
+    pd << PlotView::Data { "经典1", 0.6 };
+    pd << PlotView::Data { "经典2", 0.2 };
+    pd << PlotView::Data { "经典3", 0.3 };
+    pd << PlotView::Data { "经典4", 0.61 };
+    pd << PlotView::Data { "经典5", 0.52 };
+    pd << PlotView::Data { "经典6", 0.233 };
+
+    pd << PlotView::Data { "经典7", 0.16 };
+    pd << PlotView::Data { "经典8", 0.22 };
+    pd << PlotView::Data { "经典9", 0.33 };
+    pd << PlotView::Data { "经典10", 0.161 };
+    pd << PlotView::Data { "经典11", 0.152 };
+    pd << PlotView::Data { "经典12", 0.133 };
+
+    pd << PlotView::Data { "经典13", 0.36 };
+    pd << PlotView::Data { "经典14", 0.72 };
+    pd << PlotView::Data { "经典15", 0.3 };
+    pd << PlotView::Data { "经典16", 0.861 };
+    pd << PlotView::Data { "经典17", 0.552 };
+    pd << PlotView::Data { "经典18", 0.93 };
+    PlotView *pv = new PlotView(PlotView::Area, pd, "标题");
+ */
+
+class PlotView : public QCustomPlot
+{
+    Q_OBJECT
+public:
+    enum PlotType
+    {
+        Line,                 // 折线图
+        HistogramHorizontal,  // 横向柱状图
+        HistogramVertical,    // 竖向柱状图
+                              //        Pie,                  // QCustomPlot不支持饼图
+        Curve,                // 曲线图
+        Area,                 // 面积图
+    };
+    Q_ENUM(PlotType)
+
+    struct Data
+    {
+        QString name;
+        double value;
+
+        bool operator==(const Data &r) const { return (this->name == r.name) && (this->value == r.value); }
+    };
+
+    explicit PlotView(PlotType t, const QVector<Data> &data, const QString &title = "", QWidget *parent = nullptr);
+
+    void updateType(PlotType t);
+
+    void updateData(const QVector<Data> &data);
+
+    QPixmap toPixmap();
+
+    void plot();
+
+signals:
+
+private:
+    PlotType m_plotType;
+    QVector<Data> m_sourceData;
+    QString m_title;
+
+    void drawLine();
+    void drawHistogram(bool horizontal = false);
+    void drawCurve();
+    void drawArea();
+};
+
+#endif  // PLOTVIEW_H

+ 1 - 1
QFD/widgets/AppInfoWidget.cpp

@@ -32,7 +32,7 @@ void AppInfoWidget::initilaize()
 
     m_gridLayout = new QGridLayout();
 
-    m_verNameLabel  = new QLabel("版本:", this);
+    m_verNameLabel  = new QLabel("软件版本:", this);
     m_verValueLabel = new QLabel(this);
 
     m_contentNameLabel  = new QLabel("更新内容:", this);

+ 161 - 0
QFD/widgets/CustomPie.cpp

@@ -0,0 +1,161 @@
+#include "CustomPie.h"
+
+#include <QPainter>
+
+CustomPie::CustomPie(QWidget *parent) : QWidget(parent) { }
+
+CustomPie::~CustomPie() { }
+
+void CustomPie::paintEvent(QPaintEvent *)
+{
+    int width  = this->width();
+    int height = this->height();
+    int side   = qMin(width, height);
+
+    // 绘制准备工作,启用反锯齿,平移坐标轴中心,等比例缩放
+    QPainter painter(this);
+    painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);
+    painter.translate(width / 2, height / 2);
+    painter.scale(side / 200.0, side / 200.0);
+
+    // 绘制饼图
+    drawPie(&painter);
+}
+
+void CustomPie::drawPie(QPainter *painter)
+{
+    painter->save();
+
+    int radius = 93;
+    QRect rect(-radius, -radius, radius * 2, radius * 2);
+
+    double startAngle = 0;
+    double sum        = getSumValue();
+
+    // 逐个取出值并绘制饼图区域和对应的文字
+    int count = labels.count();
+    for (int i = 0; i < count; ++i) {
+        // 取出值并计算当前值占比面积
+        double value     = values.at(i);
+        double arcLength = value / sum * 360;
+        double percent   = value / sum * 100;
+        QRect pieRect    = rect;
+
+        // 如果当前区域展开则需要设置边框
+        painter->setPen(Qt::NoPen);
+        if (explodedIndex == i || explodedAll) {
+            painter->setPen(borderColor);
+            QPoint center = pieRect.center();
+            int mid       = startAngle + arcLength / 2;
+            center += getOffsetPoint(mid);
+            pieRect.moveCenter(center);
+        }
+
+        // 从颜色集合中取出颜色
+        painter->setBrush(colors.at(i));
+        painter->drawPie(pieRect, startAngle * 16, arcLength * 16);
+
+        QString strValue = labels.at(i);
+        if (showPercent && percent > 7) {
+            strValue = QString("%1%2%3%")
+                               .arg(strValue)
+                               .arg(strValue.isEmpty() ? "" : "\n")
+                               .arg(QString::number(percent, 'f', 0));
+        }
+
+        int mid    = startAngle + arcLength / 2;
+        int offset = 60;
+        if (percent >= 50) {
+            offset = 45;
+        } else if (percent >= 30) {
+            offset = 55;
+        } else if (percent >= 15) {
+            offset = 60;
+        }
+
+        QPoint p = getOffsetPoint(mid, offset);
+        QRect textRect;
+        textRect.setX(p.x() - 40);
+        textRect.setY(p.y() - 30);
+        textRect.setWidth(80);
+        textRect.setHeight(60);
+        painter->setPen(Qt::black);
+        // painter->drawRect(textRect);
+
+        QFont font;
+        font.setPixelSize(strValue.isEmpty() ? 20 : 17);
+        painter->setFont(font);
+
+        painter->setPen(textColor);
+        painter->drawText(textRect, Qt::AlignCenter, strValue);
+        startAngle += arcLength;
+    }
+
+    painter->restore();
+}
+
+double CustomPie::getSumValue()
+{
+    return labels.size();
+}
+
+QPoint CustomPie::getOffsetPoint(double angel, int offset)
+{
+    return QPoint {};
+}
+
+QColor CustomPie::getTextColor() const
+{
+    return textColor;
+}
+
+QColor CustomPie::getBorderColor() const
+{
+    return borderColor;
+}
+
+void CustomPie::setExplodedAll(bool explodedAll)
+{
+    this->explodedAll = explodedAll;
+}
+
+void CustomPie::setExplodedIndex(int index)
+{
+    this->explodedIndex = index;
+}
+
+void CustomPie::setDefaultColor(bool defaultColor)
+{
+    if (defaultColor) { }
+}
+
+void CustomPie::setTextColor(const QColor &textColor)
+{
+    this->textColor = textColor;
+}
+
+void CustomPie::setBorderColor(const QColor &borderColor)
+{
+    this->borderColor = borderColor;
+}
+
+void CustomPie::setColors(const QList<QColor> &colors)
+{
+    this->colors = colors;
+}
+
+void CustomPie::initPie() { }
+
+void CustomPie::appendPie(const QString &label, double value, const QString &tip)
+{
+    labels.append(label);
+    values.append(value);
+}
+
+void CustomPie::setDataPie() { }
+
+void CustomPie::loadPercent() { }
+
+void CustomPie::clearPie() { }
+
+void CustomPie::setHoleSize(double holeSize) { }

+ 81 - 0
QFD/widgets/CustomPie.h

@@ -0,0 +1,81 @@
+#ifndef CUSTOMPIE_H
+#define CUSTOMPIE_H
+
+/**
+ * 自定义饼图控件 整理:feiyangqingyun(QQ:517216493) 2019-5-21
+ * 1:可设置文字颜色
+ * 2:可设置边框颜色
+ * 3:可设置颜色集合
+ * 4:可设置某个区域是否弹出
+ * 5:可设置是否显示百分比
+ */
+#include <QWidget>
+
+class CustomPie : public QWidget
+{
+    Q_OBJECT
+    Q_PROPERTY(QColor textColor READ getTextColor WRITE setTextColor)
+    Q_PROPERTY(QColor borderColor READ getBorderColor WRITE setBorderColor)
+
+public:
+    CustomPie(QWidget *parent = 0);
+    ~CustomPie();
+
+protected:
+    void paintEvent(QPaintEvent *);
+    void drawPie(QPainter *painter);
+
+private:
+    bool explodedAll;   // 是否全部展开
+    int explodedIndex;  // 展开的索引
+
+    bool showPercent;  // 是否显示百分比
+    double holeSize;   // 空心占比
+
+    QColor textColor;      // 文字颜色
+    QColor borderColor;    // 边框颜色
+    QList<QColor> colors;  // 颜色集合
+
+    QList<QString> labels;  // 标签集合
+    QList<double> values;   // 值集合
+
+private:
+    // 获取总值
+    double getSumValue();
+    // 根据偏移值获取偏移点坐标
+    QPoint getOffsetPoint(double angel, int offset = 6);
+
+public:
+    QColor getTextColor() const;
+    QColor getBorderColor() const;
+
+public Q_SLOTS:
+    // 设置是否全部展开+展开的索引
+    void setExplodedAll(bool explodedAll);
+    void setExplodedIndex(int index);
+
+    // 设置是否启用默认颜色
+    void setDefaultColor(bool defaultColor);
+
+    // 设置文字颜色+边框颜色
+    void setTextColor(const QColor &textColor);
+    void setBorderColor(const QColor &borderColor);
+
+    // 设置颜色集合
+    void setColors(const QList<QColor> &colors);
+
+    // 初始化饼图
+    void initPie();
+    // 添加饼图数据
+    void appendPie(const QString &label, double value, const QString &tip = "");
+    // 设置数据
+    void setDataPie();
+    // 重新设置百分比
+    void loadPercent();
+    // 清除饼图
+    void clearPie();
+    // 设置空心占比
+    void setHoleSize(double holeSize);
+};
+
+#endif  // CUSTOMPIE_H

+ 272 - 0
QFD/widgets/CustomPieChart.cpp

@@ -0,0 +1,272 @@
+#include "CustomPieChart.h"
+
+CustomPieChart::CustomPieChart(QWidget *parent) : QWidget(parent)
+{
+    title = "默认标题";  // 标题名字
+    total = 0;
+
+    initPieChartWidget();
+}
+
+CustomPieChart::CustomPieChart(const QString &title, const QString &tag, const int &data, const QColor &color,
+                               QWidget *parent)
+    : QWidget(parent)
+{
+    this->title = title;  // 标题名字
+    total       = 1;      // 数据长度
+    sum         = data;   // 数据总量
+
+    addSlice(tag, data, color);
+    initPieChartWidget();
+}
+
+CustomPieChart::CustomPieChart(const QString &title, QStringList tagList, QList<int> dataList, QList<QColor> colorList,
+                               QWidget *parent)
+    : QWidget(parent)
+{
+    this->title = title;  // 标题名字
+
+    setSeries(tagList, dataList, colorList);
+    initPieChartWidget();
+}
+
+CustomPieChart::~CustomPieChart() { }
+
+/* 过滤绘制事件 */
+bool CustomPieChart::eventFilter(QObject *widget, QEvent *event)
+{
+    if (widget == pieChartWidget && event->type() == QEvent::Paint && total != 0) {
+        drawPieChart();  // 绘制饼图部件
+    } else if (widget == titleWidget && event->type() == QEvent::Paint) {
+        QPainter painter(titleWidget);
+        painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);  // 绘图和绘图文字抗锯齿
+        painter.save();
+        /* 绘制标题部件 */
+        painter.setPen(Qt::white);
+
+        titleFont.setPointSizeF(!isSetTitleFont ? titleWidget->height() * 0.75 : titleFont.pointSizeF());
+        titleFont.setWeight(!isSetTitleFont ? QFont::Bold : titleFont.weight());
+        painter.setFont(titleFont);
+        painter.drawText(QRectF(0, 0, width(), height()), title);
+
+        painter.restore();
+    }
+    return QWidget::eventFilter(widget, event);
+}
+
+/* 绘制饼图 */
+void CustomPieChart::drawPieChart()
+{
+    int width         = this->width();
+    int height        = this->height();
+    double min        = qMin(width, height);  // 宽和高中最小的值
+    double diameter   = min * 5 / 9;          // 直径
+    double radius     = diameter / 2;         // 半径
+    int startLength   = 0;                    // 起始长度
+    int midPoint      = 0;                    // 坐标原点
+    double startAngle = 0;                    // 起始角度
+
+    QPainter painter(pieChartWidget);
+    painter.setRenderHints(QPainter::Antialiasing | QPainter::TextAntialiasing);  // 绘图和绘图文字抗锯齿
+    painter.translate(width / 2, height / 2);                                     // 偏移起始点
+
+    painter.save();
+
+    fontSize = globalFont.pointSize();
+    /* 绘制总数 */
+    sumFont.setPointSizeF(!isSetSumFont ? radius / 2.5 : sumFont.pointSizeF());
+    painter.setFont(sumFont);
+    painter.setPen(QColor(54, 235, 171));
+    painter.drawText(QRectF(-radius, -radius - (radius / 4), diameter, diameter), Qt::AlignCenter,
+                     QString::number(sum));
+    /* 绘制"总数"文本 */
+    painter.setPen(Qt::white);
+    sumTextFont.setPointSizeF(!isSetSumTextFont ? radius / 5 : sumTextFont.pointSizeF());
+    painter.setFont(sumTextFont);
+    painter.drawText(QRectF(-radius, -radius + fontSize, diameter, diameter), Qt::AlignCenter, "总数");
+
+    painter.setFont(globalFont);
+    for (int count = 0; count < total; count++) {
+        startLength += (count > 0) ? int(360 * 16 * dataList[count - 1] / sum) : 0;
+        int arcLength = int(360 * 16 * dataList[count] / sum);  // 弧长
+        double angle  = 360 * dataList[count] / sum;            // 扇形的弧度
+        startAngle += (count > 0) ? 360 * dataList[count - 1] / sum : 0;
+        double radian = angle / 2 + startAngle;  // 当前弧度(所在扇形的二等分角度 + 所在扇形的起始角度)
+        double offset  = fontSize;               // 偏移量
+        double offsetX = 0;                      // x轴偏移量
+        double offsetY = 0;                      // y轴偏移量
+        /* 绘制扇形区域 */
+        painter.setPen(Qt::NoPen);
+        QRadialGradient radialGradient(midPoint, midPoint, radius);
+        radialGradient.setColorAt(0, Qt::transparent);
+        radialGradient.setColorAt(ringSize, Qt::transparent);
+        radialGradient.setColorAt((ringSize + 0.01 > 1) ? 1 : ringSize + 0.01,
+                                  qFuzzyCompare(ringSize, 1)
+                                          ? Qt::transparent
+                                          : colorList[count]);  // 从颜色列表中取出颜色并设为区域背景色
+        radialGradient.setColorAt(1, qFuzzyCompare(ringSize, 1) ? Qt::transparent : colorList[count]);
+        painter.setBrush(radialGradient);
+        painter.drawPie(QRectF(-radius, -radius, diameter, diameter), startLength, arcLength);  // 绘制饼图的扇形区域
+        /* 绘制说明 */
+        painter.setPen(colorList[count]);
+        painter.setBrush(colorList[count]);
+        legendFont.setPointSizeF(!isSetLegendFont ? radius / 5 : legendFont.pointSizeF());
+        painter.setFont(legendFont);
+        fontSize = legendFont.pointSize();
+        painter.drawRect(QRectF(-(radius * 2.1 + fontSize * 1.2), -(radius / 2.1) + count * (fontSize * 1.3),
+                                fontSize / 2, fontSize / 2));
+        painter.drawText(
+                QRectF(-(radius * 2.1), -(radius / 2.1 + fontSize / 3) + count * (fontSize * 1.3), radius, radius),
+                tagList[count]);
+        /* 绘制标签折线 */
+        tagFont.setPointSizeF(!isSetTagFont ? radius / 5 : tagFont.pointSizeF());
+        painter.setFont(tagFont);
+        fontSize      = tagFont.pointSize();
+        QPointF point = QPointF(midPoint + radius * cos(radian * M_PI / 180),
+                                midPoint - radius * sin(radian * M_PI / 180));  // 弧度在坐标轴的象限点
+        QPolygonF polygon;                                                      // 多段线
+        polygon << point;
+        QString tagText  = QString::number(dataList[count] / sum * 100, 'f', 0) + "%";
+        double textWidth = tagText.size() / 2 * fontSize;
+        if (dataList[count] != 0) {            // 数据不为零才可以绘制折线和标签
+            if (radian > 0 && radian <= 90) {  // 第一象限
+                offsetX += offset;
+                offsetY -= offset;
+                polygon << QPointF(point.x() + offsetX, point.y() + offsetY);
+                offsetX += textWidth;
+                polygon << QPointF(point.x() + offsetX, point.y() + offsetY);
+                offsetX -= textWidth;
+                offsetY -= (fontSize * 1.5);
+            } else if (radian > 90 && radian <= 180) {  // 第二象限
+                offsetX -= offset;
+                offsetY -= offset;
+                polygon << QPointF(point.x() + offsetX, point.y() + offsetY);
+                offsetX -= textWidth;
+                polygon << QPointF(point.x() + offsetX, point.y() + offsetY);
+                offsetY -= (fontSize * 1.5);
+            } else if (radian > 180 && radian <= 270) {  // 第三象限
+                offsetX -= offset;
+                offsetY += offset;
+                polygon << QPointF(point.x() + offsetX, point.y() + offsetY);
+                offsetX -= textWidth;
+                polygon << QPointF(point.x() + offsetX, point.y() + offsetY);
+            } else if (radian > 270 && radian <= 360) {  // 第四象限
+                offsetX += offset;
+                offsetY += offset;
+                polygon << QPointF(point.x() + offsetX, point.y() + offsetY);
+                offsetX += textWidth;
+                polygon << QPointF(point.x() + offsetX, point.y() + offsetY);
+                offsetX -= textWidth;
+            }
+            painter.drawPolyline(polygon);
+            /* 绘制标签 */
+            painter.drawText(QRectF(point.x() + offsetX, point.y() + offsetY, diameter, diameter),
+                             QStringLiteral("%1").arg(tagText));
+        }
+    }
+    painter.restore();
+}
+
+/* 初始化饼图部件 */
+void CustomPieChart::initPieChartWidget()
+{
+    isSetTitleFont   = false;
+    isSetTagFont     = false;
+    isSetLegendFont  = false;
+    isSetSumFont     = false;
+    isSetSumTextFont = false;
+    setRingSize(0.6);
+    /* 标题部件 */
+    titleWidget = new QWidget(this);
+    titleWidget->installEventFilter(this);
+    /* 饼图部件 */
+    pieChartWidget = new QWidget(this);
+    pieChartWidget->installEventFilter(this);
+
+    QVBoxLayout *vBoxLayout = new QVBoxLayout(this);
+    vBoxLayout->setSpacing(0);
+    vBoxLayout->setMargin(0);
+    vBoxLayout->addWidget(titleWidget, 1);
+    vBoxLayout->addWidget(pieChartWidget, 9);
+    setLayout(vBoxLayout);
+}
+
+/* 增加切片 */
+void CustomPieChart::addSlice(const QString &tag, const int &data, const QColor &color)
+{
+    tagList << tag;                                 // 标签名列表
+    dataList << data;                               // 数据列表
+    colorList << color;                             // 颜色列表
+    total = dataList.size();                        // 数据表长度
+    sum += (total == 1) ? 0 : dataList[total - 1];  // 数据总量
+}
+
+/* 设置系列 */
+void CustomPieChart::setSeries(QStringList tagList, QList<int> dataList, QList<QColor> colorList)
+{
+    total           = dataList.size();  // 数据表长度
+    sum             = 0;                // 数据总量
+    this->tagList   = tagList;          // 标签名列表
+    this->dataList  = dataList;         // 数据列表
+    this->colorList = colorList;        // 颜色列表
+    for (int count = 0; count < total; count++) {
+        sum += dataList[count];
+    }
+}
+
+/* 设置全局字体 */
+void CustomPieChart::setGlobalFont(const QFont &font)
+{
+    globalFont       = font;
+    titleFont        = font;
+    tagFont          = font;
+    legendFont       = font;
+    sumFont          = font;
+    sumTextFont      = font;
+    sumFont          = font;
+    isSetTitleFont   = true;
+    isSetTagFont     = true;
+    isSetLegendFont  = true;
+    isSetSumFont     = true;
+    isSetSumTextFont = true;
+}
+
+/* 设置标题字体 */
+void CustomPieChart::setTitleFont(const QFont &font)
+{
+    titleFont      = font;
+    isSetTitleFont = true;
+}
+
+/* 设置标签字体 */
+void CustomPieChart::setTagFont(const QFont &font)
+{
+    tagFont      = font;
+    isSetTagFont = true;
+}
+
+/* 设置说明字体 */
+void CustomPieChart::setLegendFont(const QFont &font)
+{
+    legendFont      = font;
+    isSetLegendFont = true;
+}
+/* 设置总数字体 */
+void CustomPieChart::setSumFont(const QFont &font)
+{
+    sumFont      = font;
+    isSetSumFont = true;
+}
+
+/* 设置"总数"文本字体 */
+void CustomPieChart::setSumTextFont(const QFont &font)
+{
+    sumTextFont      = font;
+    isSetSumTextFont = true;
+}
+
+/* 设置圆环大小 */
+void CustomPieChart::setRingSize(const double &ringSize)
+{
+    this->ringSize = (ringSize > 1) ? 1 : ringSize;
+}

+ 59 - 0
QFD/widgets/CustomPieChart.h

@@ -0,0 +1,59 @@
+#ifndef CUSTOMPIECHART_H
+#define CUSTOMPIECHART_H
+
+#include <QPainter>
+#include <QPaintEvent>
+#include <QVBoxLayout>
+#include <QWidget>
+#include <QLabel>
+#include <QtMath>
+
+class CustomPieChart : public QWidget
+{
+    Q_OBJECT
+
+public:
+    explicit CustomPieChart(QWidget *parent = nullptr);
+    explicit CustomPieChart(const QString &title, const QString &tag, const int &data, const QColor &color,
+                            QWidget *parent = nullptr);
+    explicit CustomPieChart(const QString &title, QStringList tagList, QList<int> dataList, QList<QColor> colorList,
+                            QWidget *parent = nullptr);
+    ~CustomPieChart();
+    void addSlice(const QString &tag, const int &data, const QColor &color);         // 增加切片的函数
+    void setSeries(QStringList tagList, QList<int> value, QList<QColor> colorList);  // 设置系列的函数
+    void setTitleFont(const QFont &font);                                            // 设置标题字体的函数
+    void setTagFont(const QFont &font);                                              // 设置标签字体的函数
+    void setLegendFont(const QFont &font);                                           // 设置说明字体的函数
+    void setSumFont(const QFont &font);                                              // 设置总数字体的函数
+    void setSumTextFont(const QFont &font);    // 设置"总数"文本字体的函数
+    void setGlobalFont(const QFont &font);     // 设置全局字体的函数
+    void setRingSize(const double &ringSize);  // 设置圆环大小的函数
+
+private:
+    int total;                                         // 总的标签数
+    double sum;                                        // 标签的总量
+    QFont globalFont;                                  // 全局字体
+    QFont titleFont;                                   // 标题字体
+    QFont tagFont;                                     // 标签字体
+    QFont legendFont;                                  // 说明字体
+    QFont sumFont;                                     // 总数文本字体
+    QFont sumTextFont;                                 // "总数"文本字体
+    bool isSetTitleFont;                               // 判断标题是否设置字体
+    bool isSetTagFont;                                 // 判断标签是否设置字体
+    bool isSetLegendFont;                              // 判断说明是否设置字体
+    bool isSetSumFont;                                 // 判断总数是否设置字体
+    bool isSetSumTextFont;                             // 判断"总数"是否设置字体
+    double fontSize;                                   // 字体大小
+    double ringSize;                                   // 圆环
+    QWidget *titleWidget;                              // 标题部件
+    QWidget *pieChartWidget;                           // 绘制饼图的部件
+    QString title;                                     // 标题名字
+    QStringList tagList;                               // 标签列表
+    QList<int> dataList;                               // 数据列表
+    QList<QColor> colorList;                           // 颜色列表
+    bool eventFilter(QObject *widget, QEvent *event);  // 过滤绘图事件的函数
+    void drawPieChart();                               // 绘制饼图的函数
+    void initPieChartWidget();                         // 初始化饼图部件的函数
+};
+
+#endif  // CUSTOMPIECHART_H