Qt-进度条插件开发

飞扬青云 大佬的博客下,意外看到一款水波纹的进度条,整体看上去很美观,感觉适用的场景也很多 (1. 单纯的进度条显示 2. 当前状态的展示,例如CPU,内存之类使用的占比),所以自己也尝试开发了一下.

这篇文章主要介绍该插件实现的逻辑,以及介绍在实现过程中使用到的库和方法

功能

对着 demo 图,我们先要确定自己需要实现哪些功能

function

  • 黑色外边框,提供圆形和正方形两种,边框宽度,大小等可调节
  • 水波纹的高度与中央显示的数字需要对应上
  • 可以对水波纹进行控制,比如水波的高度,水波的宽度,波速等等
  • 所有涉及到颜色的应该都是可自定义
  • 对字体的大小,样式等等可自定义

总结了需要实现的功能,接下来就是如何实现了

实现逻辑

先对功能进行分解,大概可以分为以下几个部分

  • 我需要实现一个带有背景色的外边框
  • 在外边框中央显示一个百分比的数字
  • 水波纹的实现

而关于每个环节中实现自定义调节功能,无非就是将涉及到的参数设置成成员函数,并提供一个 set 设置的函数而已

边框

shape

目前设定的外边框形状只有2种圆形和正方形
QPainterdrawEllipse 以及 drawRect 这2个函数就可以实现 至于边框的宽度,边框的颜色, 以及内部的颜色 QPainter 都可以设置

QPainter painter(this);
painter.setPen(QPen(边框颜色,边框宽度));
painter.setBrush( QBrush(背景颜色));

需要注意的一点是在绘制圆形和正方形, 需要提供绘图的初始位置点 (x,y), 以及绘制的宽度和高度, 因为我们边框形状只有圆形和正方形, 所以宽度和高度是一样的值, 如果调用 drawEllipse 时长宽不一致, 其实也就是椭圆了.

因为我是从图像的左上角开始绘图, 所以起始点是 (边框宽度/2, 边框宽度/2), 宽高的大小都是 总的尺寸-边框宽度, 这会存在一点问题, 因为像素是整数, 当宽度为奇数时, 会损失0.5像素, 虽然仔细看可以看出来, 但是感觉影响不大, 为了避免这个问题, 在设置边框宽度的时候添加必须为偶数的要求.

数字

数字很简单, 使用 QPainterdrawText 函数便可以实现

QPainter painter(this);
QFont font{字体样式种类, 字体大小};
painter.setPen(字体颜色);
painter.setFont(font);
painter.drawText(this->rect(), Qt::AlignCenter, 显示的数值);

水波纹

wavedemoshow

因为水波纹的形状很像 sincos 函数的图像, 将 x 方向上值通过函数计算出 y 方向的数值, 将所有的点连起来, 现在先看一下不设置填充颜色, 仅是线条情况下大致的样式

waveprogress_linedemo

边框内边距的线条蓝色的水波为线条 两个区域相交的部分(使用 intersected 接口,可以直接得到这个区域) 其实就是我们需要的水波纹区域
为了形成2层波浪的感觉, 再画一条水波, 调整它的偏移量, 在修改它颜色的透明度, 就能实现我们想要的结果

// m_size 为 整体尺寸
// m_waveHeight 波的高度
// m_waveWidth 波的宽度
// waveMidHeight 根据百分比的得到的波的位置
// m_borderWidth 边框的宽度
// m_waterwaveColor 波的颜色
// m_offset 偏移量

QPainterPath water_path1;
QPainterPath water_path2;
water_path1.moveTo((double)0,(double)m_size);
water_path2.moveTo((double)0,(double)m_size);
for (double x=0.0; x<=m_size; ++x ) {
    double waterY1 = m_waveHeight* sin(x/m_waveWidth+m_offset) + waveMidHeight;
    water_path1.lineTo(x, waterY1);

    double waterY2 = m_waveHeight* sin(x/m_waveWidth+m_offset + m_waveWidth) + waveMidHeight;
    water_path2.lineTo(x, waterY2);
}
water_path1.lineTo(m_size, m_size);
water_path2.lineTo(m_size, m_size);

QPainterPath borderpath;
if (m_shape == Shape::Round) 
    borderpath.addEllipse(m_borderWidth,m_borderWidth,m_size-2*m_borderWidth,m_size-2*m_borderWidth);
else
    borderpath.addRect(m_borderWidth,m_borderWidth,m_size-2*m_borderWidth,m_size-2*m_borderWidth);

painter.setPen(QPen(m_waterwaveColor));
QPainterPath new_path1 = borderpath.intersected(water_path1);
painter.fillPath(new_path1, QBrush(m_waterwaveColor));

QColor bg_waterwaveColor = m_waterwaveColor;
bg_waterwaveColor.setAlpha(120);
painter.setPen(QPen(bg_waterwaveColor));
QPainterPath new_path2 = borderpath.intersected(water_path2);
painter.fillPath(new_path2, QBrush(bg_waterwaveColor));

小结

作为绘制这一部分就已经完成了, 最后只需要让他们动起来, 每隔一定时间调用 update() 重绘, 在绘制的波纹的函数中(连续的)改变偏移量数值就可以像水纹一样运动起来

对于例如边框的宽度, 颜色, 或者说字体的大小样式等等, 可以通过用户设置值的传入来动态的修改整体的样式, 因为我们引入了自定义插件, 有的时候我们可能更希望这些参数是可以直接在UI界面直接填写的(designer中插件如下图), 让QT自动帮我们生成的 ui_***.h 中直接代码补全了, 所以接下来会简单介绍一下自定义插件下的 Q_PROPERTY

property

自定义插件下的 Q_PROPERTY 说明

Q_PROPERTY() 是一个宏,主要声明类中的一个属性, 一个属性行为类似于类里面的数据成员,它基于元对象系统,它还通过信号和插槽提供对象间通信。
先简单看一下用法

Q_PROPERTY(type name
        (READ getFunction [WRITE setFunction] |
         MEMBER memberName [(READ getFunction | WRITE setFunction)])
        [RESET resetFunction]
        [NOTIFY notifySignal]
        [REVISION int]
        [DESIGNABLE bool]
        [SCRIPTABLE bool]
        [STORED bool]
        [USER bool]
        [CONSTANT]
        [FINAL])

其实可以直接阅读官方文档, 文档中有很详细的介绍: https://doc.qt.io/qt-5/properties.html

QT 提供的 type 的类型已经够用, 额外需要注意的一点的是 type 类型是自定义的枚举时, 为了让 QT 可以识别, 需要调用 Q_ENUM 函数将自定义的枚举类型注册一下

class MyClass : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Priority priority READ priority WRITE setPriority NOTIFY priorityChanged)

public:
    MyClass(QObject *parent = 0);
    ~MyClass();

    enum Priority { High, Low, VeryHigh, VeryLow };
    Q_ENUM(Priority)

    void setPriority(Priority priority)
    {
        m_priority = priority;
        emit priorityChanged(priority);
    }
    Priority priority() const
    { return m_priority; }

signals:
    void priorityChanged(Priority);

private:
    Priority m_priority;
};

QPainterPath

谷歌翻译一波(凑字数,可略过):
QPainterPath 类为绘制操作提供了一个容器,可以构造和重用图形形状。 路径是由许多图形构建块组成的对象,例如矩形,椭圆,线和曲线。构建块可以在闭合的子路径中连接,例如矩形或椭圆形。闭合路径具有重合的起点和终点。或者它们可以作为未闭合的子路径独立存在,例如线条和曲线。 QPainterPath 对象可用于填充,轮廓和剪切。要为给定的painter路径生成可填充的轮廓,请使用QPainterPathStroker类。画笔路径优于普通绘图操作的主要优点是复杂的形状只需要创建一次;然后只需调用 QPainter :: drawPath() 函数就可以多次绘制它们。 QPainterPath 提供了一组函数,可用于获取有关路径及其元素的信息。此外,可以使用toReversed()函数反转元素的顺序。还有几个函数可以将此路径对象转换为多边形表示。

简单介绍一些常用的接口, 具体的接口说明直接查看官方文档

  • (椭)圆形 addEllipse
  • 正方形 addRect
  • 多边形 addPolygon
  • 弧形 arcTo 以及 arcMoveTo
  • 贝塞尔曲线 cubicTo
  • 字体 addText
  • 自定义线条一般实现的方法 moveTo(线条的起始点), lineTo(添加额外点)

还有几个函数需要注意一下:

  • QPainterPath intersected(const QPainterPath &p) const
    计算两个 QPainterPath 区域相交的部分
  • 填充的规则
    • Qt::OddEvenFill (默认)
      指定使用奇数偶数填充规则填充区域。 使用此规则,我们使用以下方法确定点是否在形状内。 绘制从该点到形状外部位置的水平线,并计算交叉点的数量。 如果交叉点的数量是奇数,则该点在形状内。 此模式是默认模式。
    • Qt::WindingFill
      指定使用非零缠绕规则填充区域。 使用此规则,我们使用以下方法确定点是否在形状内。 绘制从该点到形状外部位置的水平线。 确定每个交叉点处的线的方向是向上还是向下。 通过对每个交叉点的方向求和来确定绕组数。 如果数字不为零,则该点在形状内。 在大多数情况下,该填充模式也可以被认为是闭合形状的交叉。

官方文档介绍: https://doc.qt.io/qt-5/qpainterpath.html

代码地址

github 地址 :https://github.com/catcheroftime/WaterwaveProgressPlugin