51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

QPainter绘制一个简易的饼图(含动画和tip显示)

(文本只是一个饼图的雏形,还未进行封装,并且功能也很少,等有空我再进一步完善他。如果你需要的是一个现成的控件或完善的代码,可以在百度上找找其他的。)

先说下我的实现思路: 首先,构建Slice结构体类型,存储每个扇形的名称、值等信息。使用有序容器存储Slice,这样就能升序或者降序绘制。当数据添加进来,构建Slice并append到容器,在调用update之前对容器排序,并计算每个扇形的起始角和占的角度。在paintEvent中,遍历容器中的Slice进行绘制。对于tip的绘制,通过拦截hover和mousemove事件取得鼠标坐标,根据鼠标坐标确定tip矩形的位置,根据数据值确定tip矩形的大小。对于动画,借助Qt属性系统,增加一个百分比值,这样绘制扇形时半径乘上这个百分比值就有了逐渐变大的动画效果(如果需要角度从0到360的动画,就给角度一个百分比值)。

待完善: 1.对于每个扇形信息的显示,目前是在扇形中心位置,但是大部分饼图都有在外部显示,并用折线连接的样式,这部分比较麻烦的是文字的位置不好计算,考虑不全的话容易超出范围或者重叠;2.饼图的颜色可以增加径向渐变的效果;3.目前还没有lagend图例;4.接口封装。

本文代码链接(或直接看文末代码):

https://github.com/gongjianbo/MyTestCode/tree/master/Qt/QtPainter/SimplePieChart

代码效果:


代码展示:

代码只有一个widget的头文件和实现文件,在ui文件中我添加了测试用的按钮和输入框,就不贴出来了。

 
   
 #ifndef MAINWIDGET_H
#define MAINWIDGET_H
 
#include <QWidget>
#include <QPaintEvent>
#include <QResizeEvent>
#include <QMouseEvent>
#include <QHoverEvent>
#include <QPainter>
#include <QList>
#include <QPropertyAnimation>
 
struct PieSlice
{
    explicit PieSlice(const QString &name,double value=0.0)
        :itemName(name),itemValue(value){
    }
    QString itemName;   //项名
    double itemValue;   //项值
    double percentage;  //item值占总值的百分比
    double startAngle;  //起始角度[0,360)
    double angleSpan;   //角度值[0,360)
    QColor color;       //颜色
};
 
namespace Ui {
class MainWidget;
}
 
class MainWidget : public QWidget
{
    Q_OBJECT
    Q_PROPERTY(double pieProgress READ getPieProgress WRITE setPieProgress)
public:
    explicit MainWidget(QWidget *parent = nullptr);
    ~MainWidget();
 
    //初始化
    void init();
 
    //相关操作
    void appendSlice(const PieSlice &slice); //添加数据
    void clearSlice(); //清除容器数据
    void mouseMove(const QPoint &pos);  //hover移动
    void mouseLeave(); //hover离开
 
    //用于属性动画
    double getPieProgress() const;
    void setPieProgress(double pro); //范围[0,1]
 
protected:
    bool event(QEvent *event) override;
    void paintEvent(QPaintEvent *event) override;
    void resizeEvent(QResizeEvent *event) override;
 
private:
    Ui::MainWidget *ui;
    QPoint mousePos;      //记录鼠标hover轨迹
    bool hoveredFlag;     //是否hover
    int hoveredIndex;     //当前选择的index
 
    int pieMargin;        //边距
    QRectF pieRect;       //范围
    QPointF pieCenterPos; //中心
    int pieRadius;        //半径
    double pieValueCount; //总的值
 
    double animationProgress;      //总的进度,对应qt属性值
    int animationIndex;            //动画中的index
    double animationIndexProgress; //动画中index的进度
    QPropertyAnimation animation;
 
    QList<PieSlice> pieSliceList;  //slice数据容器
     //注意:上面的index是list容器中的index值
};
 
#endif // MAINWIDGET_H
 
   
 #include "MainWidget.h"
#include "ui_MainWidget.h"
 
#include <QtMath>
#include <QTime>
#include <QDebug>
 
MainWidget::MainWidget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::MainWidget)
{
    ui->setupUi(this);
    //开启悬停事件
    setAttribute(Qt::WA_Hover,true);
 
    init();
 
    //添加按钮
    connect(ui->btnAppend,&QPushButton::clicked,this,[this](){
        appendSlice(PieSlice{ui->editName->text(),ui->editValue->text().toDouble()});
    });
    //清除按钮
    connect(ui->btnClear,&QPushButton::clicked,this,[this](){
        clearSlice();
    });
}
 
MainWidget::~MainWidget()
{
    delete ui;
}
 
void MainWidget::init()
{
    //随机数种子
    qsrand(QTime::currentTime().msec()+QTime::currentTime().second()*1000);
    //hover标志
    hoveredIndex=-1;
    hoveredFlag=false;
    //
    pieMargin=20;
    pieCenterPos=QPoint(width()/2,height()/2);
    pieRadius=(width()<height())?(width()/2-pieMargin):(height()/2-pieMargin);
    pieRect=QRectF(pieCenterPos.x()-(pieRadius+pieMargin),pieCenterPos.y()-(pieRadius+pieMargin),
                   (pieRadius+pieMargin)*2,(pieRadius+pieMargin)*2);
    pieValueCount=0;
    //动画状态
    animationProgress=0;
    animationIndexProgress=0;
    animationIndex=0;
    animation.setTargetObject(this);
    animation.setPropertyName("pieProgress");
    animation.setDuration(1000);
    animation.setStartValue(0.0);
    animation.setEndValue(1.0);
 
    //添加测试数据
    appendSlice(PieSlice("ten_yes",10));
    appendSlice(PieSlice("five_test",5));
    appendSlice(PieSlice("nine_string",9));
    appendSlice(PieSlice("one_punch_man",1));
}
 
void MainWidget::appendSlice(const PieSlice &slice)
{
    //添加到容器
    pieSliceList.append(slice);
    //计算值的总和
    pieValueCount+=slice.itemValue;
 
    //降序排列
    std::sort(pieSliceList.begin(),pieSliceList.end(),
              [](const PieSlice&left,const PieSlice&right)->bool{
        return left.itemValue>right.itemValue;
    });
    //本来想增加一个整体的起始角度,但是后面hover时角度不好换算
    double start_angle=0; //起始角度temp,用于累加
    int h_value=-1000; //色度temp,用于相近色计算
    for(PieSlice &item:pieSliceList){
        item.percentage=item.itemValue/pieValueCount; //计算百分比
        item.startAngle=start_angle; //起始角
        item.angleSpan=item.percentage*360; //占的角度值
        start_angle+=item.angleSpan;
        //我是用的随机颜色,也可以通过传入一个颜色列表来取对应颜色
        int new_h_value=qrand()%360;
        //本来想算一个不相近的颜色,但是0附近和359附近颜色也接近
        //并且,没有考虑整体的颜色独立性
        while(qAbs(new_h_value-h_value)<50){
            new_h_value=qrand()%360;
        }
        //Hue 色度[0,359], Lightness 亮度[0,255], Saturation 饱和度[0,255]
        item.color=QColor::fromHsl(new_h_value,220,80);
        h_value=new_h_value; //用于下次计算色度相近
    }
    //update
    animation.start();
}
 
void MainWidget::clearSlice()
{
    //hover标志
    hoveredIndex=0;
    hoveredFlag=false;
    //清除数据
    pieSliceList.clear();
    pieValueCount=0;
    //动画标志
    animationIndex=0;
    animationIndexProgress=0;
    //清空后刷新
    update();
}
 
void MainWidget::mouseMove(const QPoint &pos)
{
    //不在范围内则清除hover标志
    if(!pieRect.contains(pos)){
        mouseLeave();
        return;
    }
 
    mousePos=pos;
    //计算当前所在角度
    const double arc_tan=qAtan2(pos.y()-pieCenterPos.y(),pos.x()-pieCenterPos.x());
    //aten2结果是以右侧为0点,顺时针半圆为正,逆时针半圆为负,单位是弧度?
    //需要转换为值正北为0点,顺时针增长,单位转为角度
    double arc_pos=arc_tan*180/M_PI;
    if(arc_pos<0){
        arc_pos=-arc_pos;
    }else if(arc_pos>0){
        arc_pos=360-arc_pos;
    }
 
    //计算hover选中的index
    int index=0;
    for(const PieSlice &item:pieSliceList)
    {
        if(arc_pos>=item.startAngle&&arc_pos<=(item.startAngle+item.angleSpan)){
            if(index!=hoveredIndex){
                hoveredIndex=index;
            }
            break;
        }
        ++index;
    }
    //因为由一个tip跟随鼠标移动,所以每次move都update
    hoveredFlag=true;
    update();
}
 
void MainWidget::mouseLeave()
{
    if(hoveredFlag){
        hoveredFlag=false;
        update();
    }
}
 
double MainWidget::getPieProgress() const
{
    return animationProgress;
}
 
void MainWidget::setPieProgress(double pro)
{
    //根据动画进度[0,1]计算相应的slice的index和进度值
    animationProgress=pro;
    const double item_width=1.0/pieSliceList.count();
    animationIndex=pro/item_width;
    animationIndexProgress=(pro-animationIndex*item_width)/item_width;
    update();
}
 
bool MainWidget::event(QEvent *event)
{
    switch (event->type()) {
    //拖动
    case QEvent::MouseMove:
        mouseMove(static_cast<QMouseEvent*>(event)->pos());
        return true;
        break;
        //滑动
    case QEvent::HoverMove:
        mouseMove(static_cast<QHoverEvent*>(event)->pos());
        return true;
        break;
        //离开
    case QEvent::HoverLeave:
        mouseLeave();
        return true;
        break;
    default:
        break;
    }
    return QWidget::event(event);
}
 
void MainWidget::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.fillRect(0,0,width(),height(),QBrush(QColor("black")));
 
    //抗锯齿
    painter.setRenderHint(QPainter::Antialiasing);
 
    //绘制扇形,这一部分可以和后面的tip部分分离开,
    //因为扇形变化更少,但是tip只要鼠标move了就要去update,
    //可以先把扇形绘制到一个pixmap中
    painter.save();
    painter.translate(pieCenterPos); //中心点移动到pie的中心点
    painter.setPen(QColor("white"));//绘制文本,这里没有设置字体,请自行设置
    int index=0;
    for(const PieSlice &item:pieSliceList){
        //根据动画进度计算半径当前百分比值
        const double progress=(index<animationIndex)
                ?1
               :(index>animationIndex)
                 ?0
                :animationIndexProgress;
        //半径随index递减样式
        const int radius=(pieRadius-index*5)*progress;
        //slice扇形路径
        QPainterPath path;
        path.moveTo(QPointF(0,0));
        //hover选中时半径突出一点
        if(hoveredFlag&&index==hoveredIndex){
            const QRectF hover_rect=QRectF(-(radius+10),-(radius+10),
                                           (radius+10)*2,(radius+10)*2);
            path.arcTo(hover_rect,item.startAngle,item.angleSpan);
        }else{
            const QRectF pie_rect=QRectF(-radius,-radius,radius*2,radius*2);
            path.arcTo(pie_rect,item.startAngle,item.angleSpan);
        }
        path.lineTo(QPointF(0,0));
        painter.fillPath(path,QBrush(item.color));
        //根据扇形中心点绘制文本(1/2我替换为0.6了)
        const QString text_percent=QString::number(item.percentage*100,'f',2)+"%";
        const double text_angle=item.startAngle+item.angleSpan/2; //span中心
        const int text_height=painter.fontMetrics().height()+2; //加行间隔2
        const int text_namewidth=painter.fontMetrics().width(item.itemName); //名称str宽度
        const int text_percentwidth=painter.fontMetrics().width(text_percent); //值str宽度
        const double text_x=0+radius*0.6*qCos(text_angle/180*M_PI); //文本中心点
        const double text_y=0-radius*0.6*qSin(text_angle/180*M_PI); //文本中心点
 
        //y轴qt是上负下正,所以加减操作反过来了
        painter.drawText(text_x-text_namewidth/2,text_y,item.itemName);
        painter.drawText(text_x-text_percentwidth/2,text_y+text_height,text_percent);
        ++index;
    }
    painter.restore();
 
    //绘制选中slice的tip,主要是计算坐标
    //把数据写在一个矩形框上
    if(hoveredFlag&&pieSliceList.count()>hoveredIndex&&0<=hoveredIndex){
        const int rect_margin=5; //矩形边距
        const PieSlice &item=pieSliceList.at(hoveredIndex);
        const QString str_name=item.itemName;
        const int name_width=painter.fontMetrics().width(str_name)+rect_margin*2;
        const QString str_value=QString("value:%1(%2%)")
                .arg(QString::number(item.itemValue,'f',0))
                .arg(QString::number(item.percentage*100,'f',2));
        const int text_height=painter.fontMetrics().height();
        const int value_width=painter.fontMetrics().width(str_value)+rect_margin*2;
        const int rect_height=text_height*2+rect_margin*2+2; //两行+间隔2
        const int rect_width=name_width>value_width?name_width:value_width;
        //左上角坐标,避免超出范围所以要判断并set
        QPointF top_left(mousePos.x()-rect_width,mousePos.y()-rect_height);
        if(top_left.x()<pieRect.x())
            top_left.setX(pieRect.x());
        if(top_left.y()<pieRect.y())
            top_left.setY(pieRect.y());
        //半透明矩形背景,可以fillpath绘制圆角矩形
        painter.fillRect(QRectF(top_left.x(),top_left.y(),rect_width,rect_height),
                         QBrush(QColor(150,150,150,120)));
        painter.setPen(QColor("white"));//绘制文本,这里没有设置字体,请自行设置
        painter.drawText(top_left.x()+rect_margin,
                         top_left.y()+rect_margin+text_height,
                         str_name);
        painter.drawText(top_left.x()+rect_margin,
                         top_left.y()+rect_margin+text_height*2+2,
                         str_value);
    }
    return QWidget::paintEvent(event);
}
 
void MainWidget::resizeEvent(QResizeEvent *event)
{
    //尺寸变化的时候中心和半斤等也重新计算
    pieCenterPos=QPoint(width()/2,height()/2);
    pieRadius=(width()<height())?(width()/2-pieMargin):(height()/2-pieMargin);
    pieRect=QRectF(pieCenterPos.x()-(pieRadius+pieMargin),pieCenterPos.y()-(pieRadius+pieMargin),
                   (pieRadius+pieMargin)*2,(pieRadius+pieMargin)*2);
    return QWidget::resizeEvent(event);
}

其他

  1. 国内开源:https://gitee.com/feiyangqingyun

  2. 国际开源:https://github.com/feiyangqingyun

  3. 项目大全:https://qtchina.blog.csdn.net/article/details/97565652


赞(4)
未经允许不得转载:工具盒子 » QPainter绘制一个简易的饼图(含动画和tip显示)