QML 环形进度条:Canvas 绘制与多环进度

有些进度场景不适合横条:比如加载时想要个转圈动效,或者几个指标要放在一起对比。这篇用 Canvas 画环形进度:一个单环形组件(支持正常百分比和不确定模式两种状态),再加一个多环组件。

  • 环形进度 --- 封装一个 CircleProgress 组件,用 Canvas 画轨道和进度弧
  • 多环进度 --- 同心三环,配合滑块联动,适合多指标对比

Demo 1:环形进度

一个封装好的 CircleProgress 组件:默认显示百分比,隔一会儿自动切到"加载中"的旋转模式,转几秒再切回来。

演示代码

先看组件本身,环形是怎么画出来的都在这:

qml 复制代码
import QtQuick

Item {
    id: root

    // --- 公共属性 ---
    property real value: 0.5
    property bool indeterminate: false
    property int lineWidth: 10
    property color progressColor: "#3498db"
    property color trackColor: "#e6e6e6"
    property int animationDuration: 1000

    implicitWidth: 160
    implicitHeight: 160

    readonly property real radius: Math.min(width, height) / 2 - lineWidth / 2

    // 正常进度动画
    // 注意:不加下划线前缀,否则信号名 _animatedValueChanged 会与 qmlcachegen
    // 的 signal handler 命名校验冲突(下划线属性信号的大小写规则不统一)
    property real animatedValue: value
    Behavior on animatedValue {
        enabled: !root.indeterminate // 不确定模式下禁用此动画
        NumberAnimation { duration: root.animationDuration; easing.type: Easing.OutCubic }
    }

    // Canvas 一次性绘制轨道和进度弧,重绘触发见下方 Connections
    Canvas {
        id: canvas
        anchors.fill: parent
        antialiasing: true

        onPaint: {
            var ctx = getContext("2d")
            ctx.clearRect(0, 0, width, height)

            var cx = width / 2
            var cy = height / 2
            var r = root.radius

            // 第一层:背景轨道
            ctx.beginPath()
            ctx.arc(cx, cy, r, 0, Math.PI * 2)
            ctx.strokeStyle = root.trackColor
            ctx.lineWidth = root.lineWidth
            ctx.stroke()

            // 第二层:进度圆弧
            // 不确定模式固定画 60 度的弧,靠整体旋转动起来;正常模式按进度画
            var sweep = root.indeterminate ? (Math.PI / 3) : (root.animatedValue * Math.PI * 2)
            ctx.beginPath()
            ctx.arc(cx, cy, r, -Math.PI / 2, -Math.PI / 2 + sweep)
            ctx.strokeStyle = root.progressColor
            ctx.lineWidth = root.lineWidth
            ctx.lineCap = "round"
            ctx.stroke()
        }

        // 属性变化时重绘
        Connections {
            target: root
            function onAnimatedValueChanged() { canvas.requestPaint() }
            function onIndeterminateChanged() { canvas.requestPaint() }
            function onProgressColorChanged() { canvas.requestPaint() }
            function onTrackColorChanged() { canvas.requestPaint() }
            function onLineWidthChanged() { canvas.requestPaint() }
        }

        // 不确定模式:旋转整个 Canvas,进度弧转起来(轨道是整圆,转了也看不出来)
        RotationAnimation on rotation {
            from: 0
            to: 360
            duration: 1500
            running: root.indeterminate
            loops: Animation.Infinite
        }
    }

    // 文本显示
    Text {
        anchors.centerIn: parent
        // 不确定模式下隐藏百分比
        text: root.indeterminate ? "加载中" : Math.round(root.animatedValue * 100) + "%"
        font.pixelSize: root.radius * 0.4
        font.bold: true
        color: root.progressColor
        visible: true
    }
}

然后在 demo 里用起来:

qml 复制代码
import QtQuick
import QtQuick.Layouts

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        CircleProgress {
            id: control
            Layout.preferredWidth: 220
            Layout.preferredHeight: 220
            Layout.alignment: Qt.AlignHCenter
        }

        Timer {
            id: timer
            interval: 1500
            running: true
            repeat: true
            onTriggered: {
                control.indeterminate = !control.indeterminate
            }
        }

        Item { Layout.fillHeight: true }
    }
}

关键逻辑解析

为什么用 Canvas 而不是 Shape?环形进度这种"一条弧 + 一条整圆"的图形,Shape 也能画,但它在动画(旋转)时要反复重新栅格化,容易卡、边缘容易出锯齿。Canvas 画一次就缓存成纹理,之后的旋转是场景图层面的变换,GPU 处理,既流畅又平滑。这也是项目里多环 demo 一直用的路子。

绘制逻辑全在 onPaint 里,只有两条弧:

  • 轨道:整圆 360 度,用浅色轨道色;
  • 进度弧 :从顶部(-90°)起,扫过的角度按进度算------sweep = value * 2πlineCap = "round" 让弧的两端是圆头,更精致。

弧的半径不是"组件宽度的一半",还要减去线宽的一半:

qml 复制代码
readonly property real radius: Math.min(width, height) / 2 - lineWidth / 2

不然粗线会画出组件的边界。这个减半的细节,画圆环时最容易漏。

正常模式下,进度变化走 animatedValueBehavior 动画,Connections 监听它每帧的变化调用 canvas.requestPaint() 重绘,所以进度是平滑涨上去的,百分比文字跟着变。

不确定模式是另一个玩法:indeterminate 开启时,进度弧固定画 60 度,不再重绘,而是给整个 Canvas 加一个旋转动画------弧绕着圆心转,就像加载转圈。轨道是整圆,转了也看不出来,正好省事。

组件对外只暴露了 valueindeterminatelineWidth、两个颜色等几个属性,demo 里只需一个 Timer 来回切换 indeterminate,就能看到两种状态的切换。

Demo 2:多环进度

三个同心圆环,各自代表一个指标(CPU、内存、磁盘),下面三个滑块分别控制,圆心显示综合值。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        RowLayout {
            Layout.fillWidth: true
            Layout.leftMargin: 20
            Layout.rightMargin: 20
            spacing: 30
            Layout.alignment: Qt.AlignHCenter

            // 三环进度
            ColumnLayout {
                spacing: 6
                Layout.alignment: Qt.AlignHCenter

                Text { text: "三环进度"; font.pointSize: 11; font.bold: true; color: "#555"; Layout.alignment: Qt.AlignHCenter }

                Canvas {
                    id: multiCanvas1
                    Layout.preferredWidth: 180
                    Layout.preferredHeight: 180

                    property real p1: slider1.value
                    property real p2: slider2.value
                    property real p3: slider3.value

                    onP1Changed: requestPaint()
                    onP2Changed: requestPaint()
                    onP3Changed: requestPaint()

                    onPaint: {
                        var ctx = getContext("2d")
                        ctx.clearRect(0, 0, width, height)
                        var cx = width / 2, cy = height / 2
                        var rings = [
                            { r: 75, progress: p3, color: "#6C5CE7", bg: "#E0E0E0" },
                            { r: 58, progress: p2, color: "#00B894", bg: "#E0E0E0" },
                            { r: 41, progress: p1, color: "#E17055", bg: "#E0E0E0" }
                        ]
                        for (var i = 0; i < rings.length; i++) {
                            var ring = rings[i]
                            // 背景
                            ctx.beginPath()
                            ctx.arc(cx, cy, ring.r, 0, Math.PI * 2)
                            ctx.strokeStyle = ring.bg
                            ctx.lineWidth = 10
                            ctx.stroke()
                            // 进度
                            ctx.beginPath()
                            ctx.arc(cx, cy, ring.r, -Math.PI / 2, -Math.PI / 2 + Math.PI * 2 * ring.progress)
                            ctx.strokeStyle = ring.color
                            ctx.lineWidth = 10
                            ctx.lineCap = "round"
                            ctx.stroke()
                        }
                        // 中心数值
                        ctx.fillStyle = "#333"
                        ctx.font = "bold 20px sans-serif"
                        ctx.textAlign = "center"
                        ctx.textBaseline = "middle"
                        ctx.fillText(Math.round(p2 * 100), cx, cy - 6)
                        ctx.fillStyle = "#999"
                        ctx.font = "10px sans-serif"
                        ctx.fillText("综合", cx, cy + 14)
                    }
                }
            }

            // 图例 + 滑块
            ColumnLayout {
                Layout.alignment: Qt.AlignVCenter
                spacing: 12

                RowLayout {
                    spacing: 8
                    Rectangle { width: 12; height: 12; radius: 6; color: "#E17055" }
                    Text { text: "CPU"; font.pointSize: 10; color: "#555" }
                    Text { text: Math.round(slider1.value * 100) + "%"; font.pointSize: 10; font.bold: true; color: "#E17055" }
                }
                Slider { id: slider1; from: 0; to: 1; value: 0.65; Layout.preferredWidth: 150 }

                RowLayout {
                    spacing: 8
                    Rectangle { width: 12; height: 12; radius: 6; color: "#00B894" }
                    Text { text: "内存"; font.pointSize: 10; color: "#555" }
                    Text { text: Math.round(slider2.value * 100) + "%"; font.pointSize: 10; font.bold: true; color: "#00B894" }
                }
                Slider { id: slider2; from: 0; to: 1; value: 0.78; Layout.preferredWidth: 150 }

                RowLayout {
                    spacing: 8
                    Rectangle { width: 12; height: 12; radius: 6; color: "#6C5CE7" }
                    Text { text: "磁盘"; font.pointSize: 10; color: "#555" }
                    Text { text: Math.round(slider3.value * 100) + "%"; font.pointSize: 10; font.bold: true; color: "#6C5CE7" }
                }
                Slider { id: slider3; from: 0; to: 1; value: 0.45; Layout.preferredWidth: 150 }
            }
        }

        Item { Layout.fillHeight: true }
    }
}

关键逻辑解析

多环和单环是同一个绘制思路,区别在于"画多遍":用数组描述三圈环,每圈有自己的半径、进度和颜色,循环里逐个画背景、画进度。半径从外到内依次缩小,三圈互不遮挡。

数据从哪来?Canvas 上声明了三个属性 p1/p2/p3,分别绑到三个滑块的值,每个属性变化时 requestPaint() 重绘一次。滑块一拖,对应的环就跟着涨------这就是"控件属性驱动 Canvas 重绘"的标准姿势,比在 onPaint 里到处找数据源清晰。

中心的两行文字也是用 ctx 直接画的:fillText 在圆心偏上画综合值(取中间环的进度),偏下画"综合"两个字。数值是 Canvas 画上去的,不是 QML 的 Text 组件------混着用也行,但既然画布都开了,顺手一起画更省事。

对比表格

维度 环形进度 多环进度
形态 单环 + 中心百分比 三环同心 + 中心综合值
数据来源 外部 value 三个滑块联动
不确定模式 旋转固定弧 无(有具体进度)
适合场景 单指标加载/评分 多指标仪表盘对比

运行验证

  1. 用 Qt Creator 打开 qml_progressbar/CMakeLists.txt
  2. Ctrl+R 运行;
  3. 先看环形进度自动在"百分比"和"加载中"之间切换,再到多环进度拖三个滑块,看三环联动变化。

扩展复用方向

  • CircleProgress 组件的 lineWidthprogressColor 都开放了,换主题色、调粗细都是改属性的事。
  • 多环的"环数"写死在数组里,改成动态生成就能支持任意数量的指标。
  • 圆环也可以加个圆点起点、渐变描边,做出更精致的仪表盘风格。

已验证环境

相关推荐
熬夜苦读学习15 小时前
Qt--网络编程
开发语言·qt
上海安当技术16 小时前
C/S 桌面软件怎么接 UKey 强认证?C# SDK 与 C 动态库集成 WinForms/Qt/工控实战
c语言·qt·c#
雨田言炎18 小时前
五、Qt控件使用说明大全
开发语言·qt
熬夜苦读学习19 小时前
Qt--界面优化
开发语言·qt
Bryce学亮20 小时前
FileLine,基于 Qt 6 + QML 构建的跨平台文件传输与即时通讯工具
数据库·c++·人工智能·python·qt·github
sycmancia1 天前
Qt——复习篇painter
开发语言·qt
Quz1 天前
QML ProgressBar 基础用法:默认样式与平滑动画
qt
繁重的秋春1 天前
Qt6.0下的CMake的部署配置
qt·cmake
qq_401700411 天前
Qt 程序启动太乱?重新设计你的 Application 生命周期
开发语言·qt