1、Qt Quick Shape模块自定义圆形进度条的实现

159 阅读10分钟
theme: juejin ---

通过QML提供的Shape来绘制形状,Shape使用GPU渲染。


Shape中图形形状由ShapePath决定,Shape只是ShapePath的载体。

Shape{
	ShapePath{
		......
	}
}

ShapePath使用不同的Path来描述要绘制的图形形状。

常用的有PathLine,PathArc,PathAngleArc等。


ShapePath

使用ShapePath,除了继承自父类Path的属性外,需要关注ShapePath的特有属性及内部成员的实现(具体的画图操作),以下对ShapePath的属性进行分类:

  1. 笔画的属性:
    1. 笔画宽度:strokeWidth
    2. 笔画颜色:strokeColor
    3. 笔画端点的形状:capStyle,有以下三种:
      1. ShapePath.FlatCap:方形端点,不会超出线段的覆盖范围。
      2. ShapePath.SquareCap:方形端点,超出线段的覆盖范围,超出宽度为一半的线宽。
      3. ShapePath.RoundCap:圆形端点。
    4. 笔画的虚实:strokeStyle,有以下两种:
      1. ShapePath.SolidLine:实线。
      2. ShapePath.DashLine:虚线。
    5. 两条线段的连接方式:joinStyle,有以下三种:
      1. ShapePath.MiterJoin:笔画的外边沿向外延伸,相交后对构成的空白角落进行填充。
      2. ShapePath.BevelJoin:以三角形填充两条线段相交产生的缺口。
      3. ShapePath.RoundJoin:以圆形填充两条线段相交产生的缺口。
    6. 斜交延伸:miterLimit,当joinStyle设置为ShapePath.MiterJoin时,属性规定了斜接的最大长度,默认值是2。经验证,miterLimit指的斜接长度是线宽的倍数,不是像素数。
  2. 图形的形状,由ShapePath中使用的各种Path实现(PathLine、PathAngleArc、PathArc等)。
  3. 图形内部的填充:fillRule。
    1. ShapelPath.OddEvenFill:奇偶填充规则。
    2. ShapePath.WindingFill:非零填充规则。

ShapePath实践前要了解的内容

PathLine是最容易使用的Path,它用来绘制直线。 使用PathLine便于验证ShapePath的用法。

使用PathLine,需要关注以下属性:

  1. x:终点x轴的坐标。
  2. y:终点y轴的坐标。
  3. relativeX:同x属性。
  4. relativeY:同y属性。

下一个PathLine会使用上一个PathLine的终点作为自己的起点,可以使用PathMove改变线段的起点:

PathMove{
	x:50    //更改后的x坐标。
	y:50    //更改后的y坐标。
}

ShapePath的实践---画三角形

使用ShapePath画一个三角形为例,来说明各属性的效果:

import QtQuick
import QtQuick.Window
import QtQuick.Shapes 1.5

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("triangle")

    Shape{
        id:rootShape
        anchors.fill: parent
        smooth: true
        ShapePath{
            strokeWidth: 4                  //设置笔画宽度为4个像素。
            strokeColor: "red"              //设置笔画颜色为红色。
            startX: 8                       //起点x轴位置。
            startY: 8                       //起点y轴位置。

            //第一条线,直角三角形的斜边。
            PathLine{
                x:rootShape.width - 16;
                y:rootShape.height - 16;
            }

            //第二条线,直角三角形下侧的直角边。
            PathLine{
                x:16;
                y:rootShape.height - 16;
            }

            //第三条线,直角三角形左侧的直角边。
            PathLine{
                x:16;
                y:16;
            }

            //PathLine会默认把上一个PathLine的终点,作为自己的起点。
        }
    }
}

得到如下三角形:

Pasted image 20231117134052.png

在以上图形的基础上,更改代码:

import QtQuick
import QtQuick.Window
import QtQuick.Shapes 1.5

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("triangle")

    Shape{
        id:rootShape
        anchors.fill: parent
        smooth: true
        ShapePath{
            strokeWidth: 16                  //设置笔画宽度为4个像素。
            strokeColor: "red"               //设置笔画颜色为红色。

            startX: 16                       //起点x轴位置。
            startY: 16                       //起点y轴位置。

            strokeStyle: ShapePath.DashLine  //笔画为虚线。
            capStyle: ShapePath.RoundCap     //圆形端点。

            //注意:fillGradient不能使用Gradient
            fillGradient: LinearGradient {
                //起点
                x1:16
                y1:16
                //终点
                x2:rootShape.width - 16
                y2:rootShape.height - 16

                GradientStop{position: 0.0;color: "blue"}
                GradientStop{position: 0.5;color: "black"}
                GradientStop{position: 1.0;color: "yellow"}
            }


            //第一条线,直角三角形的斜边。
            PathLine{
                x:rootShape.width - 16;
                y:rootShape.height - 16;
            }

            //第二条线,直角三角形下侧的直角边。
            PathLine{
                x:16;
                y:rootShape.height - 16;
            }

            //第三条线,直角三角形左侧的直角边。
            PathLine{
                x:16;
                y:16;
            }
        }
    }
}

得到新的图形:

Pasted image 20231117143009.png

可以观察到,三角形的各条线段的宽度明显变大,笔画变成了虚线,内部有三种颜色的填充。


线段的相交

线段相交时的填充行为由joinStyle属性决定,默认情况下使用ShapePath.BevelJoin。 笔画的strokeStyle设置为ShapePath.DashLine(虚线)时,joinStyle属性无效。

ShapePath.BevelStyle示例:

Pasted image 20231117144446.png

ShapePath.RoundJoin示例:

Pasted image 20231117144729.png

joinStyle设置为ShapePath.MiterJoin时,miterLimit属性规定了斜接的最大长度。 斜接长度指的是在两条线交汇处内角和外角之间的距离:

Pasted image 20231117145452.png

超过miterLimit规定的长度,两条线段相交时不会进行斜接。

设置miterLimit的值为1, ShapePath.MiterJoin的示例:

Pasted image 20231117150012.png

如上图所示,右下角的斜接长度超过了miterLimit乘以strackWidth的最大值,所有没有进行斜接,按照ShapePath.BevelJoin的形式来显示。


填充规则

ShapePath提供了两种填充规则:

  1. ShapelPath.OddEvenFill,奇偶填充规则: 路径包围的区域任意点P向外做一条射线,如果相交的边总数是奇数填充,反之不填充 。
  2. ShapePath.WindingFill,非零填充规则:路径包围的区域任意点P向外做一条射线,环绕数为0,如果相交的边是从左向右环绕数减1,从右向左环绕数加1,环绕数不为零填充,反之不填充 。

Pasted image 20231117152634.png


按照两种填充规则的描述,三角形无论使用哪种填充规则,都会被填充。

为了对填充规则进行验证,在大三角形内部画一个小三角形:

import QtQuick
import QtQuick.Window
import QtQuick.Shapes 1.5

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("triangle")

    Shape{
        id:rootShape
        anchors.fill: parent
        smooth: true
        ShapePath{
            strokeWidth: 8                  //设置笔画宽度为4个像素。
            strokeColor: "red"               //设置笔画颜色为红色。

            startX: 16                       //起点x轴位置。
            startY: 16                       //起点y轴位置。

            joinStyle: ShapePath.MiterJoin
            //miterLimit: 1

            //strokeStyle: ShapePath.DashLine  //笔画为虚线。
            capStyle: ShapePath.RoundCap     //圆形端点。

            //注意:fillGradient不能使用Gradient
            fillGradient: LinearGradient {
                //起点
                x1:16
                y1:16
                //终点
                x2:rootShape.width - 16
                y2:rootShape.height - 16

                GradientStop{position: 0.0;color: "blue"}
                GradientStop{position: 0.5;color: "black"}
                GradientStop{position: 1.0;color: "yellow"}
            }


            //第一条线,直角三角形的斜边。
            PathLine{
                x:rootShape.width - 16;
                y:rootShape.height - 16;
            }

            //第二条线,直角三角形下侧的直角边。
            PathLine{
                x:16;
                y:rootShape.height - 16;
            }

            //第三条线,直角三角形左侧的直角边。
            PathLine{
                x:16;
                y:16;
            }

            //在大三角形内画一个小三角形
            //设置起点。
            PathMove{
                x:100
                y:100
            }

            //小三角形的斜边。
            PathLine{
                x:200
                y:200
            }

            //小三角形下侧的直角边。
            PathLine{
                x:100
                y:200
            }

            //小三角形左侧的直角边。
            PathLine{
                x:100
                y:100
            }

            fillRule: ShapePath.OddEvenFill
        }
    }
}

使用奇偶填充规则得到的结果如下:

Pasted image 20231117154951.png

使用非零填充规则得到的结果如下:

Pasted image 20231117155138.png

构建圆形进度条

PathAngleArc

PathAngleArc是Path的一种,用来绘制圆形、椭圆形或扇形。

使用PathAngleArc需关注以下属性:

  1. real centerX:圆心的x坐标。
  2. real centerY:圆心的y坐标。
  3. bool moveToStart:标记该元素是否跟随上一个元素。
  4. real radiusX:x轴半径。
  5. real radiusY:y轴半径,x、y轴半径不相同可以画椭圆。
  6. real startAngle:开始绘制时的角度。
  7. real sweepAngle:圆弧扫过的角度。

抗锯齿

使用Shape画圆或其它带有弧度的形状时,会在图形边缘产生锯齿,如下图:

Pasted image 20231120085142.png


为了使线条更加平滑,需要使用抗锯齿功能:

全局设置

在main.c中包含:QSurfaceFormat头文件,在main函数中添加以下代码:


#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QSurfaceFormat>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QSurfaceFormat format;
    format.setSamples(4); //多重采样的次数
    QSurfaceFormat::setDefaultFormat(format);

    QQmlApplicationEngine engine;
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
        &app, []() { QCoreApplication::exit(-1); },
        Qt::QueuedConnection);
    engine.loadFromModule("untitled1", "Main");

    return app.exec();
}

这会在全局开启多重采样。


局部设置

为了节省硬件设备资源,使用Shape时,可以开启局部多重采样,既只针对需要抗锯齿的图形进行多重采样。

import QtQuick
import QtQuick.Window
import QtQuick.Shapes

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("triangle")

    Item {
        anchors.fill: parent

        layer.enabled: true                    //开启多重采样
        layer.samples: 8                       //多重采样的次数
        Shape{

            vendorExtensionsEnabled:true       //使能openGL,设置为false图形会消失
            ......
        }
    }
}

开启多重采样后的圆形边缘会更加光滑,多重采样层数越多,边缘越光滑,最多只支持8重采样

Pasted image 20231120090045.png


圆形进度条的具体实现

功能规划:

  1. 尽可能多的将属性暴露给用户,提高控件的可定制性。
  2. 控件的默认值要合理,使用户可以用最简单的方式创建并使用控件。
  3. 将整个圆形进度条分为三个主要部分:
    1. 进度条区域背景。
    2. 中心区域背景。
    3. 进度条自身。
  4. 每个主要区域提供颜色填充和渐变填充。

创建DsCycleProgressBar.qml,代码内容如下:

import QtQuick
import QtQuick.Shapes 1.5

Item{
    id:root
    width: 150
    height: 150

     //提供两种端点样式,平滑和圆形。
    enum DsProgressCapStyle{
        FlatCap = 30,
        RoundCap
    }

    //提供两种工作模式,圆环和渐变圆环。
    enum DsProgressWorkMode{
        CycleMode,
        GradientCycleMode
    }

    property real percent: 0                                    //进度条默认值。
    onPercentChanged: {
        if(percent < 0)
        {
            percent = 0
        }
        else if(percent > 100)
        {
            percent = 100
        }
    }
    Behavior on percent{
        NumberAnimation{
            duration:root.animationTime
        }
    }
    property int animationTime: 300                             //进度条动画时间。
    //进度条背景渐变,该属性被设置则cycleBackgroundFillColor属性无效,如需使cycleBackgroundFillColor属性生效,则此属性初始化为:Gradient。
    property ShapeGradient cycleBackgroundShapeGradient: LinearGradient{
        x1:0
        y1:0
        x2:progressBar.width
        y2:progressBar.height
        GradientStop{position: 0.0;color: "lightGray"}
        GradientStop{position: 1.0;color: "lightYellow"}
    }
    property color cycleBackgroundFillColor:"#007330"           //进度条部分背景颜色。
    property color strokeColor: "orange"                        //进度条颜色。
    property int capStyle: root.DsProgressCapStyle.RoundCap     //进度条端点样式。
    property real strokeWidth:width/10>20?20:width/10           //进度条笔画宽度。
    property real startAngle:90                                 //进度条起始角度。
    property real arcRadius:width/2 - strokeWidth/2             //圆半径,默认是父对象宽度的一半减去笔画宽度的一半,不建议修改。
    property bool isAnticlockwise: false                        //进度条的方向,false顺时针,true逆时针,默认顺时针转动。
    property int layerSamples:4                                 //多重采样层数。
    onLayerSamplesChanged: {
        if(value > 8)
        {
            value = 8
        }
    }
    property int borderWidth: 0                                 //进度条内外边沿的宽度。
    property color borderColor:"transparent"                    //进度条内外边沿的颜色。
    property color textColor:root.strokeColor                   //进度条文字颜色,默认与进度条同色。
    property int textFixedNumber:1                              //非零进度显示到小数点后几位,默认显示一位。
    property int textPixSize:root.width/10                      //文字像素大小,默认大小为1/10图形宽度尺寸大小。
    property string textFontFamily: "Ubuntu"                    //文字字体。
    //中心圆背景渐变,该属性被设置则centerBackgroundFillColor属性无效,如需使centerBackgroundFillColor属性生效,则此属性初始化为:Gradient。
    property ShapeGradient centerBackgroundShapeGradient: RadialGradient{
        centerX: progressBar.width/2
        centerY: progressBar.height/2
        centerRadius: progressBar.width/2
        focalX: centerX
        focalY: centerY
        GradientStop{position: 0.0;color:"lightGray"}
        GradientStop{position: 1.0;color:"lightYellow"}
    }
    property color centerBackgroundFillColor:"#007330"          //背景默认颜色。
    //工作模式,标记工作在标准模式还是在渐变模式,渐变模式下,需要提供渐变首尾两种颜色。
    property int workMode: root.DsProgressWorkMode.GradientCycleMode
    property color gradientHeadColor:"orange"                   //渐变模式下头位置的颜色。
    property color gradientMiddleColor:"#F9C603"                //渐变模式下中间位置的颜色。
    property color gradientEndColor:"orange"                    //渐变模式下尾位置的颜色。

    layer.enabled: layerSamples > 0
    layer.samples: layerSamples

    Shape{
        anchors.centerIn: parent

        vendorExtensionsEnabled:true

        width: parent.width
        height: parent.height

        //使用奇偶填充,实现进度条的背景与边框。
        ShapePath{
            id:backPath
            strokeWidth: root.borderWidth
            strokeColor: root.borderColor
            fillGradient: root.cycleBackgroundShapeGradient
            fillColor: root.cycleBackgroundFillColor
            fillRule: ShapePath.OddEvenFill
            //提供外圈的边框。
            PathAngleArc{
                id:backArc
                radiusX: root.width/2 - root.borderWidth/2
                radiusY: radiusX
                centerX: root.width/2
                centerY: root.height/2
                startAngle: 0
                sweepAngle: 360
            }
            //提供内圈的边框。
            PathAngleArc{
                radiusX: backArc.radiusX - movePath.strokeWidth - root.borderWidth
                radiusY: radiusX
                centerX: root.width/2
                centerY: root.height/2
                startAngle: 0
                sweepAngle: 360
            }
        }

        //提供CycleMode模式下的进度条。
        ShapePath{
            id:movePath
            strokeColor: root.strokeColor
            strokeWidth: root.strokeWidth
            fillColor: "transparent"
            capStyle: root.capStyle === root.DsProgressCapStyle.RoundCap?ShapePath.RoundCap:ShapePath.FlatCap
            PathAngleArc{
                id:moveArc
                radiusX: root.arcRadius - backPath.strokeWidth
                radiusY: radiusX
                centerX: root.width/2
                centerY: root.height/2
                startAngle: root.workMode !== root.DsProgressWorkMode.CycleMode?0:root.startAngle
                sweepAngle: root.percent <= 0?0:(root.workMode !== root.DsProgressWorkMode.CycleMode?0:root.isAnticlockwise?0 - root.percent * 3.6:root.percent * 3.6)
            }
        }

        //提供进度条最内部的填充,可以是颜色,也可以是渐变色。
        ShapePath{
            strokeColor: backPath.strokeColor
            strokeWidth: backPath.strokeWidth
            fillColor: root.centerBackgroundFillColor
            fillGradient: root.centerBackgroundShapeGradient
            PathAngleArc{
                centerX: root.width/2
                centerY: root.height/2
                radiusX: backArc.radiusX - movePath.strokeWidth - root.borderWidth
                radiusY: radiusX
                startAngle: 0
                sweepAngle: 360
            }
        }
    }

    Shape{
        anchors.centerIn: parent

        vendorExtensionsEnabled: true

        width: parent.width

        height: parent.height

        visible: root.workMode === root.DsProgressWorkMode.GradientCycleMode

        transform:Matrix4x4 {
            matrix: root.isAnticlockwise?Qt.matrix4x4(1, 0, 0,0,
                                                      0, 1, 0, 0,
                                                      0, 0, 1, 0,
                                                      0, 0, 0, 1):Qt.matrix4x4(-1, 0, 0, root.width,
                                                                               0, 1, 0, 0,
                                                                               0, 0, 1, 0,
                                                                               0, 0, 0, 1)
        }

        //提供GradientCycle模式下的进度条。
        ShapePath{
            strokeWidth: root.borderWidth
            strokeColor: root.borderColor

            fillGradient: ConicalGradient{
                centerX: root.width/2
                centerY: root.width/2
                angle: -root.startAngle

                GradientStop{position: 0.0;color:percent <= 0?"transparent":(percent >= 100?root.gradientHeadColor:root.gradientEndColor)}
                GradientStop{position: (percent / 100)/2;color:percent <= 0?"transparent":root.gradientMiddleColor}
                GradientStop{position: percent / 100;color:percent <= 0?"transparent":root.gradientHeadColor}
                GradientStop{position: percent / 100 + 0.00001;color:percent >= 100?root.gradientHeadColor:"transparent"}
            }

            fillRule: ShapePath.OddEvenFill
            PathAngleArc{
                id:backArc_gradient
                radiusX: root.width/2 - root.borderWidth/2
                radiusY: radiusX
                centerX: root.width/2
                centerY: root.height/2
                startAngle: 0
                sweepAngle: 360
            }

            PathAngleArc{
                radiusX: backArc_gradient.radiusX - movePath.strokeWidth - root.borderWidth
                radiusY: radiusX
                centerX: root.width/2
                centerY: root.height/2
                startAngle: 0
                sweepAngle: 360
            }
        }

        //用于在GradientCycleMode模式下,显示RoundCap不跟随移动的部分。
        ShapePath{
            strokeWidth: -1

            fillColor: root.capStyle === root.DsProgressCapStyle.RoundCap?
                           (root.percent <= 0 || root.percent >= 100?"transparent":root.gradientEndColor):"transparent"

            PathAngleArc{
                radiusX: movePath.strokeWidth / 2
                radiusY: radiusX
                centerX: root.width / 2
                centerY: root.height / 2  + moveArc.radiusX
                startAngle: root.startAngle
                sweepAngle: 180
            }
        }
    }

    //用于在GradientCycleMode模式下,显示RoundCap跟随移动的部分。
    Shape{
        anchors.centerIn: parent

        vendorExtensionsEnabled: true

        width: parent.width

        height: parent.height

        visible:root.capStyle === root.DsProgressCapStyle.RoundCap?(root.workMode === root.DsProgressWorkMode.GradientCycleMode?true:false):false

        transform:Matrix4x4 {
            matrix: root.isAnticlockwise == true?Qt.matrix4x4(-1, 0, 0, root.width,
                                                              0, 1, 0, 0,
                                                              0, 0, 1, 0,
                                                              0, 0, 0, 1):Qt.matrix4x4(1, 0, 0,0,
                                                                                       0, 1, 0, 0,
                                                                                       0, 0, 1, 0,
                                                                                       0, 0, 0, 1)
        }

        rotation: percent * 3.6

        ShapePath{
            strokeWidth: -1
            fillColor: root.percent <= 0?"transparent":root.gradientHeadColor

            PathAngleArc{
                radiusX: movePath.strokeWidth / 2
                radiusY: radiusX
                centerX: root.width / 2 + 1
                centerY: root.height / 2  + moveArc.radiusX
                startAngle: root.startAngle
                sweepAngle: 180
            }
        }
    }

    //用于显示文字。
    Text{
        anchors.centerIn: parent
        text:percent <= 0?0 + "%":(root.percent >= 100?100 + "%" : root.percent.toFixed(root.textFixedNumber).toString() + "%")
        color: root.textColor
        font.pixelSize: root.textPixSize
        font.family: root.textFontFamily
    }
}

进度条可分为有渐变色、无渐变色两种,具体使用参考上述代码中Item中的属性:

Pasted image 20231121141058.png