Qt 自绘进度条

背景

我在使用Qt开发应用程序的过程中,时不时的会使用到类似进度条的功能。时间紧迫时,便使用Qt自带的QProgressBar控件,配合样式美化一下,也能达到令人满意的效果。但是如果想使用别具一格的控件,可以使用第三方的控件,或者自己绘制。这里,我选择自己进行绘制的方式来实现。
本程序是根据之前看到的图片开发的,忘记来源是哪了,以后找到了再补上。

基本原理

原理很简单,定义一个继承自QWidget的类,并重写paintEvent函数即可。

数学基础

这里将列出本次的实现过程中涉及到的一点点的数学知识,如下:

  1. 已知圆心(x0, y0), 半径r, 圆心角θ, 求圆上对应点的坐标(x,y)。
    x = x0 + r * cos(θ)
    y = y0 + r * sin(θ)

  2. 三角函数基本性质
    sin(x)、cos(x)都是周期函数,最小周期为2π。
    sin(x)为奇函数,cos(x)为偶函数。
    正余弦的两角和与差公式
    sin(α + β) = sinαcosβ + cosαsinβ
    sin(α - β) = sinαcosβ - cosαsinβ
    cos(α + β) = cosαcosβ - sinαsinβ
    cos(α - β) = cosαcosβ + sinαsinβ

  3. 已知过圆心O(0,0)且斜率为k的直线l1,过直线l1上同一点p1(x,y)[p1在圆外],并与直线l1的夹角为θ的两条直线l2, l3,求直线l2,l3上与点p(x,y)的距离为r’的点p2,p3的坐标。
    这个问题,其实就是求以点p1(x,y)为圆心,r’为半径的圆,与过圆心的直线l2,l3的交点。【只取四个交点中的两个】
    其本质还是求圆上点的坐标。
    在这里插入图片描述在这里插入图片描述点在第二三象限时,即横坐标小于0时,α,β分别为θ+π/6,θ-π/6;
    点在第一四象限时,即橫坐标大于0时,α,β分别为π+θ+π/6,π+θ-π/6。

运行效果

在这里插入图片描述

源码

全部代码如下,实现的并不完善,很多地方并没有进行数据的有效性验证,也可能存在bug,希望发现bug的朋友不吝指正。

核心文件

头文件

mymeter.h

#ifndef MYMETER_H
#define MYMETER_H

#include <QWidget>

class MyMeter : public QWidget
{
    Q_OBJECT
public:
    explicit MyMeter(QWidget *parent = nullptr);
    virtual ~MyMeter();

    void set_start_and_span_angle(int start, int span);
    void set_arc_radius(int radius);
    void set_text(const QString & text);
    void set_show_text(bool yes);
    void set_protrude_tick_mark(bool yes);
    /* TODO */
    /* 设置颜色,最大值、最小值等的接口相对比较简单,这里不再提供了,自己实现一下即可 */
    /* 其实是我偷懒啦,我也没有实现这部分 */

protected:
    void resizeEvent(QResizeEvent * event) override;
    void paintEvent(QPaintEvent * event) override;
    void draw_background(QPainter & painter);    /* 绘制部件背景 */
    void draw_negative(QPainter & painter);      /* 绘制底片 */
    void draw_positive(QPainter & painter);      /* 绘制当前内容 */

public slots:
    void set_value(qint16 value);

signals:

private:
    qint16   my_start_angle;        /* 起始角度 */
    qint16   my_span_angle;         /* 跨越角度 */
    qint16   my_stop_angle;         /* 终止边角度 */
    qreal    my_angle;              /* 每单位所跨角度 */
    qreal    my_cur_angle;          /* 当前角度 */
    qint16   my_minimum;            /* 最小值 */
    qint16   my_maximum;            /* 最大值 */
    qint16   my_value;              /* 当前值 */
    qint16   my_arc_width;          /* 圆弧宽度 */
    qint16   my_arc_radius;         /* 圆弧半径 */
    qint16   my_arc_circle_space;   /* 圆弧内侧与中心圆间的间距 */
    qint16   my_arc_tick_mark_space;/* 圆弧外侧与刻度内侧的间距 */
    qint16   my_tick_mark_length;   /* 刻度线度 */
    qint16   my_tick_mark_count;    /* 突出显示刻度线的数量 */
    qint16   my_tick_mark_len_delta;/* 突出显示的相邻刻度线长度的差值 */
    QString  my_text;               /* 提示文本 */
    bool     my_show_text;          /* 是否显示功能提示文本 */
    bool     my_protrude_tick_mark; /* 是否突出显示当前刻度线 */
    QColor   my_background_color;   /* 背景色 */
    QColor   my_color;              /* 前景色 文本颜色 */
};

#endif // MYMETER_H
实现文件

mymeter.cpp

#include <QPainter>
#include <QResizeEvent>
#include <QtMath>
#include "mymeter.h"

MyMeter::MyMeter(QWidget *parent) :
    QWidget(parent),
    my_start_angle(315),
    my_span_angle(270),
    my_minimum(0),
    my_maximum(100),
    my_value(0),
    my_arc_width(20),
    my_arc_radius(100),
    my_arc_circle_space(20),
    my_arc_tick_mark_space(10),
    my_tick_mark_length(10),
    my_tick_mark_count(4),
    my_tick_mark_len_delta(2),
    my_show_text(true),
    my_protrude_tick_mark(true),
    my_background_color(QColor(198, 239, 206)),
    my_color(QColor(66, 202, 107))
{
    my_angle = (qreal)(my_span_angle)/(my_maximum - my_minimum);
    my_stop_angle = my_start_angle > 180 ? 720 - (my_span_angle + my_start_angle) : 360 - (my_start_angle + my_span_angle);
    my_cur_angle = my_stop_angle;

    my_text = QString(QStringLiteral("进度"));
}

MyMeter::~MyMeter()
{

}

void MyMeter::set_start_and_span_angle(int start, int span)
{
    my_start_angle = start;
    my_span_angle = span;
    my_stop_angle = my_start_angle > 180 ? 720 - (my_span_angle + my_start_angle) : 360 - (my_start_angle + my_span_angle);
}

void MyMeter::set_arc_radius(int radius)
{
    my_arc_radius = radius;
}

void MyMeter::set_text(const QString &text)
{
    my_text = text;
}

void MyMeter::set_show_text(bool yes)
{
    my_show_text = yes;
}

void MyMeter::set_protrude_tick_mark(bool yes)
{
    my_protrude_tick_mark = yes;
}

void MyMeter::resizeEvent(QResizeEvent *event)
{
    int half_min_width = my_arc_radius
            + (my_arc_width>>1)
            + my_arc_tick_mark_space
            + my_tick_mark_length
            + my_tick_mark_count * my_tick_mark_len_delta; /* 突出显示刻度线时,多出来的最大长度 */
    resize((half_min_width<<1), (half_min_width<<1));
}

void MyMeter::paintEvent(QPaintEvent *event)
{
    QPainter painter(this);
    painter.setRenderHint(QPainter::Antialiasing);
    painter.translate(width()>>1, height()>>1);

    /* 1. 窗体背景 */
    draw_background(painter);

    /* 2. 静态部分 */
    draw_negative(painter);

    /* 3. 动态部分 */
    draw_positive(painter);
}

void MyMeter::draw_background(QPainter &painter)
{
    painter.save();

    painter.restore();
}

void MyMeter::draw_negative(QPainter &painter)
{
    painter.save();

    /* 1. 刻度 */
    painter.setPen(QPen(my_background_color, 2));
    int tick_mark_inner_radius = my_arc_radius + (my_arc_width>>1) + my_arc_tick_mark_space ;
    int tick_mark_outer_radius = my_arc_radius + (my_arc_width>>1) + my_arc_tick_mark_space + my_tick_mark_length;
    int total = my_maximum - my_minimum + 1;
    for (int i = 0; i < total; ++i) {
        QPointF p_start(tick_mark_inner_radius * qCos((my_stop_angle + i*my_angle)*M_PI/180),
                        tick_mark_inner_radius * qSin((my_stop_angle + i*my_angle)*M_PI/180));
        QPointF p_stop(tick_mark_outer_radius * qCos((my_stop_angle + i*my_angle)*M_PI/180),
                       tick_mark_outer_radius * qSin((my_stop_angle + i*my_angle)*M_PI/180));
        painter.drawLine(p_start, p_stop);
    }

    /* 2. 背景进度条 */
    painter.setPen(QPen(my_background_color, my_arc_width, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    painter.drawArc(-my_arc_radius, -my_arc_radius, my_arc_radius<<1, my_arc_radius<<1, my_start_angle<<4, my_span_angle<<4);

    /* 3. 中间渐变圆 */
    int circle_radius = my_arc_radius - my_arc_circle_space - (my_arc_width>>1);
    QLinearGradient linear_ball(QPointF(0, circle_radius>>1), QPointF(0, -circle_radius>>1));
    my_background_color.setAlpha(128);
    my_color.setAlpha(128);
    linear_ball.setColorAt(0.0, my_background_color);
    linear_ball.setColorAt(1.0, my_color);
    painter.setBrush(linear_ball);
    painter.setPen(Qt::NoPen);
    painter.drawEllipse(QPointF(0, 0), circle_radius, circle_radius);

    my_background_color.setAlpha(255);
    my_color.setAlpha(255);

    painter.restore();
}

void MyMeter::draw_positive(QPainter &painter)
{
    painter.save();

    /* 1 刻度 */
    painter.setPen(QPen(my_color, 2));
    int tick_mark_inner_radius = my_arc_radius + (my_arc_width>>1) + my_arc_tick_mark_space ;
    int tick_mark_outer_radius = my_arc_radius + (my_arc_width>>1) + my_arc_tick_mark_space + my_tick_mark_length;
    int upper(0);
    if (my_protrude_tick_mark) {
        upper = my_value + 1 - my_tick_mark_count;
        for (int i = my_value; i > my_value - my_tick_mark_count && i >= 0; --i) {
            int tick_mark_different_outer_radius = tick_mark_outer_radius + (my_tick_mark_count + i - my_value) * my_tick_mark_len_delta;
            QPointF p_start(tick_mark_inner_radius * qCos((my_stop_angle + i*my_angle)* M_PI/180),
                            tick_mark_inner_radius * qSin((my_stop_angle + i*my_angle) * M_PI/180));
            QPointF p_stop(tick_mark_different_outer_radius * qCos((my_stop_angle + i*my_angle)*M_PI/180),
                           tick_mark_different_outer_radius * qSin((my_stop_angle + i*my_angle)*M_PI/180));
            painter.drawLine(p_start, p_stop);
        }
    }
    else
        upper = my_value + 1 - my_tick_mark_count;

    for (int i = 0; i < upper; ++i) {
        QPointF p_start(tick_mark_inner_radius * qCos((my_stop_angle + i*my_angle)* M_PI/180),
                        tick_mark_inner_radius * qSin((my_stop_angle + i*my_angle) * M_PI/180));
        QPointF p_stop(tick_mark_outer_radius * qCos((my_stop_angle + i*my_angle)*M_PI/180),
                       tick_mark_outer_radius * qSin((my_stop_angle + i*my_angle)*M_PI/180));
        painter.drawLine(p_start, p_stop);
    }

    /* 2. 动态进度条 */
    QLinearGradient gradient(QPointF(my_arc_radius * qCos(my_stop_angle*M_PI/180),
                                     my_arc_radius * qSin(my_stop_angle*M_PI/180)),
                             QPointF(my_arc_radius * qCos(my_cur_angle*M_PI/180),
                                     my_arc_radius * qSin(my_cur_angle*M_PI/180)));
    gradient.setColorAt(0.0, my_background_color);
    gradient.setColorAt(1.0, my_color);
    painter.setPen(QPen(gradient, my_arc_width, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin));
    painter.drawArc(-my_arc_radius, -my_arc_radius, my_arc_radius<<1, my_arc_radius<<1,
                    -my_stop_angle<<4, -(my_cur_angle-my_stop_angle)*16);

    /* 3. 进度指针 */
    /* 3.1 指示弧线 设置弧线所对圆心角为20度 */
    int circle_angle(20);
    int half_angle(circle_angle >> 1);
    int pointer_arc_radius = my_arc_radius - ((my_arc_width + my_arc_circle_space)>>1);
    gradient.setStart(QPointF(pointer_arc_radius * qCos((my_cur_angle - half_angle)*M_PI/180),
                              pointer_arc_radius * qSin((my_cur_angle - half_angle)*M_PI/180)));
    gradient.setFinalStop(QPointF(pointer_arc_radius * qCos((my_cur_angle + half_angle)*M_PI/180),
                                  pointer_arc_radius * qSin((my_cur_angle + half_angle)*M_PI/180)));
    gradient.setColorAt(0.0, my_background_color);
    gradient.setColorAt(0.4, my_color);
    gradient.setColorAt(0.6, my_color);
    gradient.setColorAt(1.0, my_background_color);
    painter.setPen(QPen(gradient, 6, Qt::SolidLine, Qt::RoundCap));
    painter.drawArc(-pointer_arc_radius, -pointer_arc_radius,
                    pointer_arc_radius<<1, pointer_arc_radius<<1,
                    -(my_cur_angle - half_angle)*16, -circle_angle<<4);         /* 顺时针计算 */

    /* 3.2 等边三角形 BEGIN */
    painter.setPen(QPen(my_color, 1, Qt::SolidLine, Qt::SquareCap, Qt::MiterJoin));
    painter.setBrush(my_color);
    int space(2);                                 /* 进度弧线内侧与顶点间的距离 建议值域为[0, 2] */
    int vertex((my_arc_circle_space>>1) - space); /* 三角形靠近刻度线的顶点 与 指示弧线中心的距离 */
    QPointF p1((pointer_arc_radius + vertex) * qCos(my_cur_angle*M_PI/180),
               (pointer_arc_radius + vertex) * qSin(my_cur_angle*M_PI/180));

    /* 根据斜率求出正切角度 */
    qreal k0, theta;
    if (-0.0000001 > p1.x() || 0.0000001 < p1.x()) {
        k0 = p1.y()/p1.x();
        theta = qAtan(k0);    // (-PI/2, PI/2)
    }
    else
        theta = M_PI/2;
    qreal alpha = theta + M_PI/6;
    qreal beta = theta - M_PI/6;
    if (0 < p1.x()) {
        alpha = M_PI + alpha;
        beta = M_PI + beta;
    }
    QPointF p2(p1.x() + vertex * qCos(alpha), p1.y() + vertex * qSin(alpha));
    QPointF p3(p1.x() + vertex * qCos(beta), p1.y() + vertex * qSin(beta));
    QPointF pf[4] = {p1, p2, p3, p1};
    painter.drawPolygon(pf, 4);
    /* 三角形 END */

    /* 4. 提示文本 */
    QString text(QString::number(my_value) + "%");
    int circle_radius = my_arc_radius - my_arc_circle_space - (my_arc_width>>1);
    QFont ft = painter.font();
    ft.setPointSize(circle_radius/2);
    ft.setBold(true);
    painter.setFont(ft);
    QFontMetrics fm(ft);
    painter.setPen(QPen(my_color, 2));
    painter.drawText(-fm.horizontalAdvance(text)>>1, 0, text);

    ft.setPointSize(circle_radius/3);
    ft.setBold(true);
    painter.setFont(ft);
    QFontMetrics fm2(ft);
    painter.drawText(-fm2.horizontalAdvance(my_text)>>1, fm2.height(), my_text);

    painter.restore();
}

void MyMeter::set_value(qint16 value)
{
    my_value = value;

    int theta =  my_start_angle > 180 ? 720 - (my_span_angle + my_start_angle) : 360 - (my_start_angle + my_span_angle);
    my_cur_angle = theta + (my_value - my_minimum) * my_angle;

    update();
}

测试文件

头文件

testwidget.h

#ifndef TESTWIDGET_H
#define TESTWIDGET_H

#include <QWidget>
#include <QTimer>
#include <QGridLayout>
#include "mymeter.h"

class TestWidget : public QWidget
{
    Q_OBJECT
public:
    explicit TestWidget(QWidget *parent = nullptr);
    virtual ~TestWidget();

signals:

private:
    QTimer * my_timer;
    MyMeter * my_process;
    MyMeter * my_cpu;
    MyMeter * my_disk;
    MyMeter * my_hygrometer;
    QGridLayout * my_gridlayout;
};
实现文件

testwidget.cpp

#include <QRandomGenerator>
#include "testwidget.h"

TestWidget::TestWidget(QWidget *parent) : QWidget(parent)
{
    setWindowTitle(QStringLiteral("测试仪表"));
    setFixedSize(600, 600);
    my_pro = new MyMeter;
    my_pro->set_arc_radius(60);
    my_cpu = new MyMeter;
    my_cpu->set_text("CPU");
    my_cpu->set_arc_radius(70);
    my_disk = new MyMeter;
    my_disk->set_text("磁盘");
    my_disk->set_arc_radius(80);
    my_hygrometer = new MyMeter;
    my_hygrometer->set_text("湿度");
    my_hygrometer->set_start_and_span_angle(0, 270);

    my_gridlayout = new QGridLayout;
    my_gridlayout->addWidget(my_pro, 0, 0);
    my_gridlayout->addWidget(my_cpu, 0, 1);
    my_gridlayout->addWidget(my_disk, 1, 0);
    my_gridlayout->addWidget(my_hygrometer, 1, 1);
    setLayout(my_gridlayout);

    my_timer = new QTimer(this);
    my_timer->setInterval(500);
    connect(my_timer, &QTimer::timeout, [=]{
        my_pro->set_value(QRandomGenerator::global()->generate() % 101);
        my_cpu->set_value(QRandomGenerator::global()->generate() % 101);
        my_disk->set_value(QRandomGenerator::global()->generate() % 101);
        my_hygrometer->set_value(QRandomGenerator::global()->generate() % 101);
    });
    my_timer->start();
}

TestWidget::~TestWidget()
{
    if (my_timer) {
        delete my_timer;
        my_timer = nullptr;
    }

    if (my_pro) {
        delete my_pro;
        my_pro = nullptr;
    }

    if (my_cpu) {
        delete my_cpu;
        my_cpu = nullptr;
    }

    if (my_disk) {
        delete my_disk;
        my_disk = nullptr;
    }

    if (my_gridlayout) {
        delete my_gridlayout;
        my_gridlayout = nullptr;
    }
}

main文件主要只有两三行代码

#include "testwidget.h"

    TestWidget tw;
    tw.show();
  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值