Use QPainter under Qt to realize the drawing of pie chart and donut chart on the interface


foreword

The previous article described the use of the Charts module under Qt to draw pie charts: QChart realizes the drawing of pie charts and ring charts at specified positions on the ui interface , but it cannot achieve the desired effect well during use , and the drawing event in Qt can solve this problem, so I decided to use QPainter and QPaintEvent to realize the custom drawing of pie chart and donut chart. Here I wrote a simple example and showed the relevant code for everyone to learn , If there are any mistakes, everyone is welcome to criticize and correct.

Project effect
Please add a picture description


提示:以下是本篇文章正文内容,下面案例可供参考

1. Example explanation

First add the relevant header files:

#include <QtMath>   //后文中计算文本位置使用
#include <QPainter>

Add the paintEvent function to the class and rewrite it in cpp (see below)

protected:
    void paintEvent(QPaintEvent *);

In this example, the data structure of each area of ​​the donut chart is as follows, including the name, color and quantity of the area:

struct PieData
{
    
    
    QString name;   //名称
    int num;        //数量
    QColor color;   //颜色

    PieData(QString name,int num,QColor color)
    {
    
    
        this->name = name;
        this->num = num;
        this->color = color;
    }
};

Parameters of the donut chart:

int m_radius;         //外圆半径
int m_innerWidth;     //圆环内径
QPoint m_center;      //圆心坐标
qreal m_startAngle;   //圆环绘制起点
int m_textDistance;   //文本与圆心的距离
qreal m_totality;     //总数
QVector<PieData> m_vData;   //数据容器

Example function interface:
Please add a picture description

2. The steps of drawing the donut chart

1. Use drawRoundedRect to draw the rounded background of the pie chart.
2. Obtain the proportion of each area * 360 to get the coverage angle of this area. Use drawPie to complete the drawing of each area, and the drawing direction is counterclockwise.
3. Write the area name and quantity text , and use drawLine to connect the text with the boundary of the corresponding area, so here you need to determine the start and end positions of the connection line 4. Use drawPoint to
draw the end point of the connection line. If you need to set the end point as a hollow circle, you need to use The drawEllipse combination realizes
5. Draw the inner circle and change the pie chart into a donut chart. For detailed steps, see the following code:

//重写绘图事件
void Widget::paintEvent(QPaintEvent *)
{
    
    
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing,true);   //抗锯齿

    //绘制圆角背景
    painter.setBrush(Qt::white);
    painter.setPen(Qt::NoPen);   //去除背景边框
    painter.drawRoundedRect(10,10,360,360,8,8);

    //绘制饼图
    qreal startAngle = m_startAngle;   //绘制起点
    qreal spanAngle = 0;   //各区域占比,覆盖角度
    for(int i=0;i<m_vData.size();i++)
    {
    
    
        painter.setPen(m_vData[i].color);
        painter.setBrush(m_vData[i].color);
        if(m_totality)   //防止总数为0
        {
    
    
            spanAngle = m_vData[i].num * 360 / m_totality;
        }
        painter.drawPie(m_center.x() - m_radius,m_center.y() - m_radius,m_radius * 2,m_radius * 2,startAngle * 16,spanAngle * 16);
        startAngle += spanAngle;
    }

    //绘制区域名称和占比
    startAngle = m_startAngle;
    spanAngle = 0;
    for(int i=0;i<m_vData.size();i++)
    {
    
    
        painter.setPen(QColor("#333333"));
        painter.setFont(QFont("STSongti-SC-Bold, STSongti-SC",16));
        if(m_totality)
        {
    
    
            spanAngle = m_vData[i].num * 360 / m_totality;
        }
        int textAngle = startAngle + spanAngle / 2;
        QString text = QString("%1").arg(m_vData[i].name);
        int textWidth = painter.fontMetrics().horizontalAdvance(text);
        int textHeight = painter.fontMetrics().height();
        int textX = m_center.x() + m_textDistance * qCos(textAngle * M_PI / 180) - textWidth / 2;
        int textY = m_center.y() - m_textDistance * qSin(textAngle * M_PI / 180) + textHeight / 2;
        startAngle += spanAngle;

        //绘制文本
        QRect rect(textX,textY - textHeight,textWidth + 10,textHeight * 2);
        painter.drawText(rect,Qt::AlignCenter,text);

        //绘制连接线,文本要靠近对应区域,需要修改连接线终点位置
        painter.setPen(m_vData[i].color);
        int lineStartX = m_center.x() + (m_radius - 10) * qCos(textAngle * M_PI / 180);
        int lineStartY = m_center.y() - (m_radius - 10) * qSin(textAngle * M_PI / 180);
        int lineEndX = 0;
        int lineEndY = 0;
        if(textX < lineStartX)   //文本在左边
        {
    
    
            //可自行根据实际进行位置偏移的修改
            lineEndX = textX + textWidth/2;
            if(textY < lineStartY)   //文本在上边
            {
    
    
                lineEndY = textY + textHeight + 5;
            }
            else
            {
    
    
                lineEndY = textY - textHeight - 5;
            }
        }
        else
        {
    
    
            lineEndX = textX + textWidth/2;
            if(textY < lineStartY)
            {
    
    
                lineEndY = textY;
            }
            else
            {
    
    
                lineEndY = textY - textHeight - 5;
            }
        }
        painter.drawLine(lineStartX,lineStartY,lineEndX,lineEndY);

        //绘制终点
        painter.setPen(QPen(m_vData[i].color,5));
        painter.drawPoint(lineEndX,lineEndY);

        //将终点设为空心圆
        //painter.drawEllipse(QPoint(lineEndX,lineEndY),5,5);
        //painter.setPen(QPen(QColor("#FFFFFF"),1));
        //painter.setBrush(QColor("#FFFFFF"));
        //painter.drawEllipse(QPoint(lineEndX,lineEndY),3,3);
    }

    //绘制内圆,将饼图变为圆环
    painter.setPen(QPen(QColor("#FFFFFF"),10));
    painter.setBrush(QColor("#FFFFFF"));
    painter.drawEllipse(m_center,m_innerWidth,m_innerWidth);
}

3. Set the donut chart data

First define a QVector container to save the name, color and quantity of the data in each area of ​​the donut chart. Here use findChild and combine the control name of the input box to find the corresponding QLineEdit, get the input value and store it in the container, and use it after setting the pie chart data update() to update the drawing event

//更新饼图
void Widget::refreshChart()
{
    
    
    //设置圆环图各区域数据名称及颜色
    QVector<PieData> vData;
    QColor m_colors[5] = {
    
    QColor("#0286FF"),QColor("#FFA784"),QColor("#7EBBFF"),QColor("#DFEEFF"),QColor("#85D9FD")};
    QLineEdit *leNum;
    for(int i=0;i<5;i++)
    {
    
    
        leNum = ui->wg_test->findChild<QLineEdit *>("le_num_" + QString::number(i+1));   //找到对应lineedit
        if(leNum != 0)
        {
    
    
            int num = leNum->text().toInt();
            if(num > 0)   //过滤输入为空或0的数据
            {
    
    
                PieData data = PieData(QString("数据%1\n(%2)").arg(i+1).arg(num),num,m_colors[i]);
                vData.push_back(data);
            }
        }
    }
    this->setPieData(vData);
}

//设置饼图数据
void Widget::setPieData(QVector<PieData> vData)
{
    
    
    //获取数据
    m_vData = vData;

    //获取总数
    m_totality = 0;
    for(int i=0;i<m_vData.size();i++)
    {
    
    
        m_totality += m_vData[i].num;
    }
    this->update();
}

4. Example complete code

1.widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QtMath>
#include <QPainter>

QT_BEGIN_NAMESPACE
namespace Ui {
    
     class Widget; }
QT_END_NAMESPACE

struct PieData
{
    
    
    QString name;   //名称
    int num;        //数量
    QColor color;   //颜色

    PieData(QString name,int num,QColor color)
    {
    
    
        this->name = name;
        this->num = num;
        this->color = color;
    }
};

class Widget : public QWidget
{
    
    
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr);
    ~Widget();

    void initWidget();

    //圆环图各参数函数接口
    void setRadius(int radius);
    void setInnerWidth(int width);
    void setCenter(QPoint center);
    void setStartAngle(qreal startAngle);
    void setTextDistance(int textDistance);
    void setPieData(QVector<PieData> vData);

    void refreshChart();

protected:
    void paintEvent(QPaintEvent *);

private slots:
    void on_pb_test_clicked();

private:
    Ui::Widget *ui;

    int m_radius;         //外圆半径
    int m_innerWidth;     //圆环内径
    QPoint m_center;      //圆心坐标
    qreal m_startAngle;   //圆环绘制起点
    int m_textDistance;   //文本与圆心的距离
    qreal m_totality;     //总数
    QVector<PieData> m_vData;   //数据容器
};
#endif // WIDGET_H

2.widget.cpp

#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    
    
    ui->setupUi(this);
    this->initWidget();
}

Widget::~Widget()
{
    
    
    delete ui;
}

//初始化界面
void Widget::initWidget()
{
    
    
    //初始化变量
    m_radius = 0;
    m_innerWidth = 0;
    m_center = QPoint(0,0);
    m_startAngle = 0;
    m_textDistance = 0;
    m_totality = 0;
    m_vData.clear();
}

//设置外圆半径
void Widget::setRadius(int radius)
{
    
    
    m_radius = radius;
}

//设置圆环内径
void Widget::setInnerWidth(int width)
{
    
    
    m_innerWidth = width;
}

//设置圆心
void Widget::setCenter(QPoint center)
{
    
    
    m_center = center;
}

//设置圆环绘制起点
void Widget::setStartAngle(qreal startAngle)
{
    
    
    m_startAngle = startAngle;
}

//设置文本与圆心的距离
void Widget::setTextDistance(int textDistance)
{
    
    
    m_textDistance = textDistance;
}

//设置饼图数据
void Widget::setPieData(QVector<PieData> vData)
{
    
    
    //获取数据
    m_vData = vData;

    //获取总数
    m_totality = 0;
    for(int i=0;i<m_vData.size();i++)
    {
    
    
        m_totality += m_vData[i].num;
    }
    this->update();
}

//更新饼图
void Widget::refreshChart()
{
    
    
    //设置圆环图各区域数据名称及颜色
    QVector<PieData> vData;
    QColor m_colors[5] = {
    
    QColor("#0286FF"),QColor("#FFA784"),QColor("#7EBBFF"),QColor("#DFEEFF"),QColor("#85D9FD")};
    QLineEdit *leNum;
    for(int i=0;i<5;i++)
    {
    
    
        leNum = ui->wg_test->findChild<QLineEdit *>("le_num_" + QString::number(i+1));   //找到对应lineedit
        if(leNum != 0)
        {
    
    
            int num = leNum->text().toInt();
            if(num > 0)   //过滤输入为空或0的数据
            {
    
    
                PieData data = PieData(QString("数据%1\n(%2)").arg(i+1).arg(num),num,m_colors[i]);
                vData.push_back(data);
            }
        }
    }
    this->setPieData(vData);
}

//重写绘图事件
void Widget::paintEvent(QPaintEvent *)
{
    
    
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing,true);   //抗锯齿

    //绘制圆角背景
    painter.setBrush(Qt::white);
    painter.setPen(Qt::NoPen);   //去除背景边框
    painter.drawRoundedRect(10,10,360,360,8,8);

    //绘制饼图
    qreal startAngle = m_startAngle;   //绘制起点
    qreal spanAngle = 0;   //各区域占比,覆盖角度
    for(int i=0;i<m_vData.size();i++)
    {
    
    
        painter.setPen(m_vData[i].color);
        painter.setBrush(m_vData[i].color);
        if(m_totality)   //防止总数为0
        {
    
    
            spanAngle = m_vData[i].num * 360 / m_totality;
        }
        painter.drawPie(m_center.x() - m_radius,m_center.y() - m_radius,m_radius * 2,m_radius * 2,startAngle * 16,spanAngle * 16);
        startAngle += spanAngle;
    }

    //绘制区域名称和占比
    startAngle = m_startAngle;
    spanAngle = 0;
    for(int i=0;i<m_vData.size();i++)
    {
    
    
        painter.setPen(QColor("#333333"));
        painter.setFont(QFont("STSongti-SC-Bold, STSongti-SC",16));
        if(m_totality)
        {
    
    
            spanAngle = m_vData[i].num * 360 / m_totality;
        }
        int textAngle = startAngle + spanAngle / 2;
        QString text = QString("%1").arg(m_vData[i].name);
        int textWidth = painter.fontMetrics().horizontalAdvance(text);
        int textHeight = painter.fontMetrics().height();
        int textX = m_center.x() + m_textDistance * qCos(textAngle * M_PI / 180) - textWidth / 2;
        int textY = m_center.y() - m_textDistance * qSin(textAngle * M_PI / 180) + textHeight / 2;
        startAngle += spanAngle;

        //绘制文本
        QRect rect(textX,textY - textHeight,textWidth + 10,textHeight * 2);
        painter.drawText(rect,Qt::AlignCenter,text);

        //绘制连接线,文本要靠近对应区域,需要修改连接线终点位置
        painter.setPen(m_vData[i].color);
        int lineStartX = m_center.x() + (m_radius - 10) * qCos(textAngle * M_PI / 180);
        int lineStartY = m_center.y() - (m_radius - 10) * qSin(textAngle * M_PI / 180);
        int lineEndX = 0;
        int lineEndY = 0;
        if(textX < lineStartX)   //文本在左边
        {
    
    
            //可自行根据实际进行位置偏移的修改
            lineEndX = textX + textWidth/2;
            if(textY < lineStartY)   //文本在上边
            {
    
    
                lineEndY = textY + textHeight + 5;
            }
            else
            {
    
    
                lineEndY = textY - textHeight - 5;
            }
        }
        else
        {
    
    
            lineEndX = textX + textWidth/2;
            if(textY < lineStartY)
            {
    
    
                lineEndY = textY;
            }
            else
            {
    
    
                lineEndY = textY - textHeight - 5;
            }
        }
        painter.drawLine(lineStartX,lineStartY,lineEndX,lineEndY);

        //绘制终点
        painter.setPen(QPen(m_vData[i].color,5));
        painter.drawPoint(lineEndX,lineEndY);

        //将终点设为空心圆
        //painter.drawEllipse(QPoint(lineEndX,lineEndY),5,5);
        //painter.setPen(QPen(QColor("#FFFFFF"),1));
        //painter.setBrush(QColor("#FFFFFF"));
        //painter.drawEllipse(QPoint(lineEndX,lineEndY),3,3);
    }

    //绘制内圆,将饼图变为圆环
    painter.setPen(QPen(QColor("#FFFFFF"),10));
    painter.setBrush(QColor("#FFFFFF"));
    painter.drawEllipse(m_center,m_innerWidth,m_innerWidth);
}

//测试
void Widget::on_pb_test_clicked()
{
    
    
    //设置圆环各参数
    this->setRadius(100);
    this->setInnerWidth(70);   //设为0即为饼图
    this->setCenter(QPoint(180,180));
    this->setStartAngle(90);   //区域绘制方向为逆时针
    this->setTextDistance(150);
    this->refreshChart();
}

3.widget.ui
Please add a picture description

5. Download link

My example Baidu network disk link: https://pan.baidu.com/s/1q4S87YnMxhUd3w1l0Yr4tA
Extraction code: xxcj


Summarize

This example describes in detail the use of QPinter to draw a ring chart. The difficulty lies in the connection between the text of each area and the middle position of the area boundary. Here, the basic mathematical trigonometric function is used to obtain the relevant value. The text position in the example is still The offset should be modified according to the actual situation. In addition, you can see the function interface in it. Have you thought of anything here? In fact, modify it into a custom control and use it as a component. In subsequent use, you only need to upgrade the widget object on the UI interface or yourself to this control, and you can easily realize the drawing of the donut chart. As for how to modify it , I won’t introduce it here. The way to implement custom controls can be seen in the article I wrote before: (1) Two ways for Qt to implement custom controls - promotion method


hello:
Learn together and make progress together. If you still have related questions, you can leave a message in the comment area for discussion.

Reference blog: QPainter draws pie charts

Guess you like

Origin blog.csdn.net/XCJandLL/article/details/131089682