How to create a circular progress bar in pure QML+JS?

17,636

Solution 1

I've implemented a basic circular progress using a Canvas.

enter image description here

import QtQml 2.2
import QtQuick 2.0

// draws two arcs (portion of a circle)
// fills the circle with a lighter secondary color
// when pressed
Canvas {
    id: canvas
    width: 240
    height: 240
    antialiasing: true

    property color primaryColor: "orange"
    property color secondaryColor: "lightblue"

    property real centerWidth: width / 2
    property real centerHeight: height / 2
    property real radius: Math.min(canvas.width, canvas.height) / 2

    property real minimumValue: 0
    property real maximumValue: 100
    property real currentValue: 33

    // this is the angle that splits the circle in two arcs
    // first arc is drawn from 0 radians to angle radians
    // second arc is angle radians to 2*PI radians
    property real angle: (currentValue - minimumValue) / (maximumValue - minimumValue) * 2 * Math.PI

    // we want both circle to start / end at 12 o'clock
    // without this offset we would start / end at 9 o'clock
    property real angleOffset: -Math.PI / 2

    property string text: "Text"

    signal clicked()

    onPrimaryColorChanged: requestPaint()
    onSecondaryColorChanged: requestPaint()
    onMinimumValueChanged: requestPaint()
    onMaximumValueChanged: requestPaint()
    onCurrentValueChanged: requestPaint()

    onPaint: {
        var ctx = getContext("2d");
        ctx.save();

        ctx.clearRect(0, 0, canvas.width, canvas.height);

        // fills the mouse area when pressed
        // the fill color is a lighter version of the
        // secondary color

        if (mouseArea.pressed) {
            ctx.beginPath();
            ctx.lineWidth = 1;
            ctx.fillStyle = Qt.lighter(canvas.secondaryColor, 1.25);
            ctx.arc(canvas.centerWidth,
                    canvas.centerHeight,
                    canvas.radius,
                    0,
                    2*Math.PI);
            ctx.fill();
        }

        // First, thinner arc
        // From angle to 2*PI

        ctx.beginPath();
        ctx.lineWidth = 1;
        ctx.strokeStyle = primaryColor;
        ctx.arc(canvas.centerWidth,
                canvas.centerHeight,
                canvas.radius,
                angleOffset + canvas.angle,
                angleOffset + 2*Math.PI);
        ctx.stroke();


        // Second, thicker arc
        // From 0 to angle

        ctx.beginPath();
        ctx.lineWidth = 3;
        ctx.strokeStyle = canvas.secondaryColor;
        ctx.arc(canvas.centerWidth,
                canvas.centerHeight,
                canvas.radius,
                canvas.angleOffset,
                canvas.angleOffset + canvas.angle);
        ctx.stroke();

        ctx.restore();
    }

    Text {
        anchors.centerIn: parent

        text: canvas.text
        color: canvas.primaryColor
    }

    MouseArea {
        id: mouseArea

        anchors.fill: parent
        onClicked: canvas.clicked()
        onPressedChanged: canvas.requestPaint()
    }
}

Solution 2

I found a kinda elegant solution in plain QML which can be also used for styling a regular QtQuick ProgressBar component. The idea behind this is to use a ConicalGradient on a border-only Rectangle.

Here is the code:

import QtQuick 2.3
import QtQuick.Controls.Styles 1.2
import QtGraphicalEffects 1.0

ProgressBarStyle
{
   panel : Rectangle
   {
      color: "transparent"
      implicitWidth: 80
      implicitHeight: implicitWidth

      Rectangle
      {
         id: outerRing
         z: 0
         anchors.fill: parent
         radius: Math.max(width, height) / 2
         color: "transparent"
         border.color: "gray"
         order.width: 8
      }

      Rectangle
      {
         id: innerRing
         z: 1
         anchors.fill: parent
         anchors.margins: (outerRing.border.width - border.width) / 2
         radius: outerRing.radius
         color: "transparent"
         border.color: "darkgray"
         border.width: 4

         ConicalGradient
         {
            source: innerRing
            anchors.fill: parent
            gradient: Gradient
            {
               GradientStop { position: 0.00; color: "white" }
               GradientStop { position: control.value; color: "white" }
               GradientStop { position: control.value + 0.01; color: "transparent" }
               GradientStop { position: 1.00; color: "transparent" }
            }
         }
      }

      Text
      {
         id: progressLabel
         anchors.centerIn: parent
         color: "black"
         text: (control.value * 100).toFixed() + "%"
      }
   }
}

enter image description here

Solution 3

I came across an example by Diego Dotta on GitHub using two rotating circles that seems to work nicely for this use case. It involves setting the duration of a PropertyAnimation. So while this works well for a timer that you can set, it would need a different approach for something you didn't know how long it would take. This is tweaked a bit and ported to QtQuick 2.0:

main.qml:

import QtQuick 2.0
import Ubuntu.Components 0.1

Rectangle {
    width: units.gu(50)
    height: units.gu(50)

    property int seconds : 0

    LoadCircle {
        id: circle
        anchors.centerIn: parent
        loadtimer: 10*1000 // 10 seconds
        Component.onCompleted: start();
        onFinishedChanged: {
            timer.stop();
            borderColor = "green"
        }
    }

    Rectangle {
        id : theTimer
        anchors.centerIn: parent
        width : units.gu(10) ; height: units.gu(10)

        Label { 
            text: seconds
            font.bold: true
            fontSize: "x-large"
            anchors.centerIn: parent
        }
    }

    Timer {
        id: timer
        interval: 1000; running: true; repeat: true;
        onTriggered: seconds++;
    }

}

LoadCircle.qml:

import QtQuick 2.0
import Ubuntu.Components 0.1

Row{
    id: circle

    property int loadtimer: 4000
    property color circleColor: "transparent"
    property color borderColor: "red"
    property int borderWidth: 10
    property alias running: initCircle.running
    property bool finished: false;

    width: units.gu(30)
    height: width

    function start(){
        part1.rotation = 180
        part2.rotation = 180
        initCircle.start()
    }

    function stop(){
        initCircle.stop()
    }

    Item{
        width: parent.width/2
        height: parent.height
        clip: true

        Item{
            id: part1
            width: parent.width
            height: parent.height
            clip: true
            rotation: 180
            transformOrigin: Item.Right

            Rectangle{
                width: circle.width-(borderWidth*2)
                height: circle.height-(borderWidth*2)
                radius: width/2
                x:borderWidth
                y:borderWidth
                color: circleColor
                border.color: borderColor
                border.width: borderWidth
                smooth: true
            }
        }
    }

    Item{
        width: parent.width/2
        height: parent.height
        clip: true

        Item{
            id: part2
            width: parent.width
            height: parent.height
            clip: true

            rotation: 180
            transformOrigin: Item.Left

            Rectangle{
                width: circle.width-(borderWidth*2)
                height: circle.height-(borderWidth*2)
                radius: width/2
                x: -width/2
                y: borderWidth
                color: circleColor
                border.color: borderColor
                border.width: borderWidth
                smooth: true
            }
        }
    }
    SequentialAnimation{
        id: initCircle
        PropertyAnimation{ target: part2; property: "rotation"; to:360; duration:loadtimer/2 }
        PropertyAnimation{ target: part1; property: "rotation"; to:360; duration:loadtimer/2 }
        ScriptAction { script: finished = true; }
    }
}

example image

Share:
17,636

Related videos on Youtube

Nik
Author by

Nik

Updated on June 19, 2022

Comments

  • Nik
    Nik about 2 years

    My application is made using QML+JS and I am looking to create a circular progress bar widget. I can create the circle using a QML Rectangle and settings its radius equal to its width/2 to make it into a circle. How do I create a progress bar out of it?

    I am planning to implement the following mockup.

    enter image description here

    • iBelieve
      iBelieve about 10 years
      Canvas maybe? I fiddled with that a bit when working on some UI stuff for the Ubuntu Stock Ticker app, and I think that it might work for what you want.
    • Nik
      Nik about 10 years
      @iBelieve Yeah I am realising that. Hoping someone has some code with a circular progress bar ready since I have used Canvas before, so it makes it much harder for me to implement.
  • Nik
    Nik about 10 years
    Hmm, the code author uses 60 rectangle (in the shape of a circle) to simulate the progress. I am not sure how smooth that will appear. I will try it out and comment back here about it.
  • Nik
    Nik about 10 years
    Hey thanks. This is what I ended up implementing few days back after finding a tutorial online. Do you mind adding some comments to your code especially in the onPaint() function to make it clear for everyone? I can then accept your answer. Thanks in advance.
  • Nik
    Nik about 10 years
    Thanks for the solution. I found a shorter simpler way to achieve this using QML Canvas. But I can nonetheless keep this solution in mind in case I cannot implement something in the canvas method.
  • Charles
    Charles about 10 years
    Done. Does the change suit you ?
  • BaCaRoZzo
    BaCaRoZzo over 9 years
    Nice idea and nicer overall look!
  • George Thomas
    George Thomas over 7 years
    its drawing a square for me!
  • George Thomas
    George Thomas over 7 years
    how to do it with an image
  • George Thomas
    George Thomas over 7 years
    the four sides are slightly cutoff.. how to make it a perfect circle
  • George Thomas
    George Thomas over 7 years
    How can i get import Ubuntu.Components 0.1
  • rbaleksandar
    rbaleksandar over 7 years
    @GeorgeThomas It's because of the radius size and it being too big. If you take for example Math.min(canvas.width, canvas.height)/4 it will fit perfectly (but will be smaller than the whole parent component that it fills up). Just adjust it according to the size of its parent and make it smaller (also take into account the width of the lines).
  • warranty_void
    warranty_void about 5 years
    @George: if it's showing a square for you then there's probably a problem associated to this line: "radius: Math.max(width, height) / 2". Does it work if you use an absolute radius value there?
  • peco
    peco over 3 years
    nice idea, just to mention it can be simplified further by replacing the Inner and outer circles with Image which is circle shape with default color. And you can set source to be image of the ConicalGradient.