user17726418
user17726418

Reputation: 2363

How to apply QLinearGradient on a QWidget with round borders

I am trying to animate a timer progress on a QPushButton border, using stylesheets and QLinearGradient.

Here's how I'm doing it:

#include <QApplication>
#include <QPushButton>
#include <QTimer>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    QPushButton button("Animation Button");

    float greenPosition = 0.99;
    float whitePosition = 0.01;
    QTimer timer;
    enum Border {Top = 1, Left = 2, Bottom = 3, Right = 4};
    int border = 1;

    button.connect(&timer, &QTimer::timeout, [&button, &greenPosition, &whitePosition, &border]()
    {
        //Left or Bottom
        if(border == 2 || border == 3)
        {
            if(whitePosition + 0.01 > 1)
            {
                whitePosition = 0;
                border = border++ % 4 + 1;
            }
            whitePosition += 0.01;
        }
        else
        {
            if(greenPosition - 0.01 < 0.01)
            {
                greenPosition = 1;
                border = border % 4 + 1;
            }
            greenPosition -= 0.01;
        }

        switch (border)
        {
            case Top:
                button.setStyleSheet(QString("border: 2px solid white;"
                                           //"border-radius: 5px;"
                                           "border-top: 2px solid qlineargradient(x0:0, x2:1,"
                                           "stop: 0 green,"
                                           "stop: %1 green,"
                                           "stop: %2 white,"
                                           "stop: 1 white);").arg(greenPosition).arg(greenPosition + 0.01));
                break;
            case Left:
                button.setStyleSheet(QString("border: 2px solid white;"
                                           //"border-radius: 5px;"
                                           "border-left: 2px solid qlineargradient(y0:0, y2:1,"
                                           "stop: 0 white,"
                                           "stop: %1 white,"
                                           "stop: %2 green,"
                                           "stop: 1 green);").arg(whitePosition).arg(whitePosition + 0.01));

                break;
            case Bottom:
                button.setStyleSheet(QString("border: 2px solid white;"
                                           //"border-radius: 5px;"
                                           "border-bottom: 2px solid qlineargradient(x0:0, x2:1,"
                                           "stop: 0 white,"
                                           "stop: %1 white,"
                                           "stop: %2 green,"
                                           "stop: 1 green);").arg(whitePosition).arg(whitePosition + 0.01));
                break;
            case Right:
                button.setStyleSheet(QString("border: 2px solid white;"
                                           //"border-radius: 5px;"
                                           "border-right: 2px solid qlineargradient(y0:0, y2:1,"
                                           "stop: 0 green,"
                                           "stop: %1 green,"
                                           "stop: %2 white,"
                                           "stop: 1 white);").arg(greenPosition).arg(greenPosition + 0.01));
                break;
        }
    });

    timer.start(50);
    button.show();

    return a.exec();
}

Here's the logic I came up with, to create this animation:

greenPosition and whitePosition are values that control green and white color range in QLinearGradient, green is the progress, and white is the actual border color.

The basic idea, is that I'm making the white recede, and the green advance, or vice versa.

There is a small fraction between their values to avoid the gradient effect, hence why I'm adding 0.01. If greenPosition or whitePosition reach the limit of the [1.00 - 0.00] range, they reset to the default.

border is a variable that allows me to iterate through the 4 borders in my switch case, just a simple circular counter.

All of this loops using a QTimer.

Here's the result:

Animation button

I need round borders, but when I use them, it looks like this:

Border animation

The borders seem to form another side of the button on their own, as if with round borders, the button now has 8 sides.

This is because the animation on the round borders seem to be synced with the border that's currently being animated, instead of being animated when the animation reaches them. It also looks like a mirror effect.

Note: This is just an observation to explain how it looks, not what actually happens.

Here's how it looks if I apply a green color without QLinearGradient while using round borders:

Normal colored round border

How do I animate the round borders the same way as the not-round ones?

Upvotes: 0

Views: 316

Answers (2)

user17726418
user17726418

Reputation: 2363

Since setting border radius using stylesheets has unwanted effects, you could make your widget borders round without them, and use paintEvent instead; here's how:

You could draw a rounded rect with a color same as the widget parent's background, this acts as if it's hiding those sharp corners, and make them look round.

One downside of this method is that it limits how round the corners can get, because if the painter draws a too rounded rect, it will mask the widget itself or cause sharp edges.

It is possible to slightly get around that by increasing the border size, to get more space, thus, making it possible to increase the border radius when using a painter without ruining the widget edges.

There is an upside for using this method. It prevents the widget background from poking out the border.

Here's an example of subclass derived from QPushButton with a custom paintEvent:

class button : public QPushButton
{
    Q_OBJECT

public:
    button (QWidget *parent = nullptr) : QPushButton(parent) {}


protected:
    void paintEvent(QPaintEvent *event) override
    {
        QPushButton::paintEvent(event);

        QRectF r = rect();
        r.adjust(-1,-1,+1,+1);

        QPainter p(this);
        QColor color = parentWidget()->palette().brush(QPalette::Window).color();
        p.setPen(QPen(color,2));
        p.setRenderHint(QPainter::Antialiasing);
        p.drawRoundedRect(r, 6, 6);
    }
};

Here's the result, where the bottom button is a normal QPushButton with no border radius, just to increase the difference visibility:

Round corners using paintEvent

Upvotes: 0

kumarvhat
kumarvhat

Reputation: 39

For these types of features, QML provides a very flexible tool set.

I've achieved what I think you are looking for in QML:

Canvas {
        id: border_animation
        property real progress: 0
        onProgressChanged: {
            border_animation.requestPaint()
        }
        anchors.centerIn: parent
        height: parent.height*0.10
        width: parent.width*0.65
        z: 2
        Text {
            text: "start timer"
            color: "white"
            font.bold: true
            font.pointSize: 15
            anchors.centerIn: parent
        }
        NumberAnimation on progress {
            id: startTimer
            from: 0
            to: 1
            running: false
            duration: 1000
            onFinished: {
                startTimer.start()
            }
        }
        onPaint: {
            var ctx = border_animation.getContext('2d')

            ctx.strokeStyle = "green"
            ctx.fillStyle = "black"
            ctx.lineWidth = 2

            ctx.beginPath()
            ctx.rect(0,0,width,height)
            ctx.fill()

            var Lcenterx = width*0.25
            var Lcentery = height*0.50
            var Rcenterx = width*0.75
            var Rcentery = height*0.50
            var radius = height*0.40

            //each section is 25% of the border so we break it up that way
            if(progress<=0.25){
                var topProgress = progress/0.25
                var rightProgress = 0
                var bottomProgress = 0
                var leftProgress = 0
            }else if(progress<=0.50){
                topProgress = 1
                rightProgress = (progress-0.25)/0.25
                bottomProgress = 0
                leftProgress = 0
            }else if(progress<=0.75){
                topProgress = 1
                rightProgress = 1
                bottomProgress = (progress-0.50)/0.25
                leftProgress = 0
            }else{
                topProgress = 1
                rightProgress = 1
                bottomProgress = 1
                leftProgress = (progress-0.75)/0.25
            }

            //top line
            ctx.strokeStyle = "orange"
            ctx.beginPath();
            ctx.moveTo(Lcenterx, Lcentery-radius);
            ctx.lineTo(Lcenterx + (Rcenterx - Lcenterx)*topProgress, Rcentery-radius);
            ctx.stroke();

            ctx.strokeStyle = "yellow"
            ctx.beginPath();
            ctx.arc(Rcenterx, Rcentery, radius, 1.5*Math.PI, 1.5*Math.PI + Math.PI * rightProgress);
            ctx.stroke();

            //bottom line
            ctx.strokeStyle = "cyan"
            ctx.beginPath();
            ctx.moveTo(Rcenterx, Lcentery+radius);
            ctx.lineTo(Rcenterx - (Rcenterx - Lcenterx)*bottomProgress, Rcentery+radius);
            ctx.stroke();

            ctx.strokeStyle = "green"
            ctx.beginPath();
            ctx.arc(Lcenterx, Lcentery, radius, 0.5*Math.PI, 0.5*Math.PI + Math.PI * leftProgress);
            ctx.stroke();

        }
        MouseArea {
            anchors.fill: parent
            onClicked: {
                startTimer.start()



            }
        }
    }

animating

animating again

Upvotes: -1

Related Questions