文章目录
- 前言
- 一、示例讲解
- 二、圆环图绘制步骤
- 三、设置圆环图数据
- 四、示例完整代码
- 五、下载链接
- 总结
前言
前面的文章有讲述使用Qt下的Charts 模块来进行饼图的绘制:QChart实现ui界面上指定位置饼状图、圆环图的绘制,但是使用过程中并不能很好的实现自己想要的效果,而Qt中的绘图事件可以解决这个问题,所以决定使用QPainter及QPaintEvent来实现饼状图及圆环图的自定义绘制,在这里编写了一个简单的示例,并将相关代码展现出来以便大家学习,如有错误之处,欢迎大家批评指正。
项目效果
提示:以下是本篇文章正文内容,下面案例可供参考
一、示例讲解
首先添加相关头文件:
#include <QtMath> //后文中计算文本位置使用
#include <QPainter>
类中添加paintEvent函数,并在cpp中进行重写(见后文)
protected:
void paintEvent(QPaintEvent *);
本示例中圆环图各区域数据结构体如下,其中有区域的名称颜色及数量:
struct PieData
{
QString name; //名称
int num; //数量
QColor color; //颜色
PieData(QString name,int num,QColor color)
{
this->name = name;
this->num = num;
this->color = color;
}
};
圆环图各参数:
int m_radius; //外圆半径
int m_innerWidth; //圆环内径
QPoint m_center; //圆心坐标
qreal m_startAngle; //圆环绘制起点
int m_textDistance; //文本与圆心的距离
qreal m_totality; //总数
QVector<PieData> m_vData; //数据容器
示例函数接口:
二、圆环图绘制步骤
1.使用drawRoundedRect实现饼状图圆角背景的绘制
2.获取各区域的占比*360得到此区域覆盖角度,使用drawPie完成各区域的绘制,绘制方向为逆时针
3.进行区域名称和数量文本的绘制,并使用drawLine将文本与对应区域的边界进行连接,所以这里要确定连接线的起点和终点位置
4.使用drawPoint来绘制连接线的终点,如果需要将终点设为空心圆,就需要使用drawEllipse组合实现
5.进行内圆的绘制,将饼图变为圆环图,详细的步骤见下列代码:
//重写绘图事件
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);
}
三、设置圆环图数据
首先定义一个QVector容器来保存圆环图各区域数据的名称,颜色及数量,这里使用了findChild并结合输入框的控件名来找到对应QLineEdit,获取输入值存入容器,设置好饼图数据后使用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::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();
}
四、示例完整代码
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
五、下载链接
我的示例百度网盘链接:https://pan.baidu.com/s/1q4S87YnMxhUd3w1l0Yr4tA
提取码:xxcj
总结
这个示例详细的讲述了使用QPinter进行圆环图的绘制,其中难点在于各区域文本与区域边界中间位置的连接,这里也是使用到了基础的数学三角函数来求取相关值,示例中的文本位置还是要根据实际进行偏移量的修改。另外可以看到其中的函数接口,这里你有没有想到什么呢?其实将其修改为自定义控件,当作一个组件来使用,后续使用只需要在ui界面上或者自己添加的widget对象提升为该控件,就可以很方便的实现圆环图的绘制,至于如何修改,这里就不进行介绍了,实现自定义控件的方式可以看看我之前写的文章:(一)Qt实现自定义控件的两种方式—提升法
hello:
共同学习,共同进步,如果还有相关问题,可在评论区留言进行讨论。
参考博客:QPainter绘制饼图