Home > database >  Qt Custom Animated Button
Qt Custom Animated Button

Time:07-14

I am trying to create custom animated button I found this page: Buttons Example

I liked 3 & 19 on this website. I did try to make 3 but it is not the same. Can someone help me?

Gif

My Code for 3rd button on website:

 # -*- coding: utf-8 -*-
import sys, os, time, math
from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *


class EButton3(QPushButton):
    AnimateEnabled = True
    Radius = 10

    _m_Text = ""
    def __init__(self, parent=None):
        super(EButton3, self).__init__(None)
        self.enterEvent = self.Custom_enterEvent
        self.leaveEvent = self.Custom_leaveEvent
        self.setText("Button")
    def getText(self):
        return self._m_Text
    def setText(self, Text):
        self._m_Text = Text
        self.update()

    _m_hover=False
    def isHover(self):
        return self._m_hover

    def paintEvent(self, event: QPaintEvent):
        ret = None #QPushButton.paintEvent(self, event)
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        path, path2 = QPainterPath(), QPainterPath()
        BaseBackground, BaseBackgroundHover = QColor(Qt.black), QColor(22,2,22)
        BaseForeground, BaseForegroundHover = QColor(Qt.white), QColor(Qt.black)
        painter.setBrush(QBrush(BaseBackground if not self.isHover() else BaseBackgroundHover))
        painter.setPen(Qt.NoPen)

        rect = QRectF(0, 0, self.width(), self.height())
        path.addRoundedRect(rect, self.Radius, self.Radius)
        painter.drawPath(path)
        painter.setPen(BaseForeground if not self.isHover() else BaseForegroundHover)


        if self.AnimateEnabled:
            painter.setBrush(QBrush(QColor(231, 231, 231)))
            anval = self.AnimateVal / 100
            polygon = QPolygonF([
                QPoint(self.width() * anval, 0),
                QPoint(0, 0),
                QPoint(0, self.height()),
                QPoint((self.width()   10) * math.sin(anval / 100 * 180), self.height())
            ])
            painter.setClipPath(path)
            path2.addPolygon(polygon)

            painter.drawPath(path2)

        painter.drawText(self.rect(), Qt.AlignCenter, self.getText())
        return ret

    _animateVal = 0
    def setAnimateVal(self, val):
        self._animateVal = val
    def getAnimateVal(self):
        return self._animateVal

    AnimateVal = QtCore.Property(int, getAnimateVal, setAnimateVal)
    temps = []

    def Custom_enterEvent(self, event) -> None:
        self._m_hover = True
        step2_ani = QPropertyAnimation(self, b'AnimateVal')
        step2_ani.setStartValue(self.getAnimateVal())
        step2_ani.setEndValue(100)
        step2_ani.setEasingCurve(QEasingCurve.InQuad)
        step2_ani.setDuration(500)
        self.temps.append(step2_ani)

        def valChanged():
            self.update()

        def finished():
            self.AnimateVal = 100

        step2_ani.valueChanged.connect(valChanged)
        step2_ani.finished.connect(finished)
        step2_ani.start()
        return QPushButton.enterEvent(self, event)

    def Custom_leaveEvent(self, event) -> None:
        self._m_hover = False
        step2_ani = QPropertyAnimation(self, b'AnimateVal')
        self.temps.append(step2_ani) # we need to store it or self.step_ani2 = ... else it will not start
        step2_ani.setStartValue(self.getAnimateVal())
        step2_ani.setEndValue(0)
        step2_ani.setEasingCurve(QEasingCurve.OutCubic)
        step2_ani.setDuration(500)

        def valChanged():
            self.update()

        def finished():
            self.AnimateVal = 0

        step2_ani.valueChanged.connect(valChanged)
        step2_ani.finished.connect(finished)
        step2_ani.start()
        return QPushButton.leaveEvent(self, event)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    wind = QMainWindow()
    wind.setStyleSheet("QMainWindow{background-color:rgb(247,247,250)}")
    wind.resize(150, 80)
    wid = QWidget()
    lay = QHBoxLayout(wid)
    lay.setAlignment(Qt.AlignCenter)

    mycustombutton = EButton3()
    mycustombutton.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
    lay.addWidget(mycustombutton)
    wind.setCentralWidget(wid)
    wind.show()
    sys.exit(app.exec())

My Code for 19th: button

# -*- coding: utf-8 -*-
import sys, os, time, math
from PySide6 import QtCore, QtWidgets, QtGui
from PySide6.QtWidgets import *
from PySide6.QtCore import *
from PySide6.QtGui import *


class EButton19(QPushButton):
    AnimateEnabled = True
    Radius = 10

    _m_Text = ""
    def __init__(self, parent=None):
        super(EButton19, self).__init__(None)
        self.enterEvent = self.Custom_enterEvent
        self.leaveEvent = self.Custom_leaveEvent
        self.setText("Button")
        self.propertyanimation = QPropertyAnimation(self, b'AnimateVal')
        self.propertyanimation.setDuration(350)

    def getText(self):
        return self._m_Text
    def setText(self, Text):
        self._m_Text = Text
        self.update()

    _m_hover=False
    def isHover(self):
        return self._m_hover

    def paintEvent(self, event: QPaintEvent):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        BaseBackground, BaseBackgroundHover = QColor(231,231,231), QColor(Qt.black)
        BaseForeground, BaseForegroundHover = QColor(Qt.black), QColor(Qt.white)
        path, path2 = QPainterPath(), QPainterPath()
        painter.setBrush(QBrush(BaseBackground if not self.isHover() else BaseBackgroundHover))
        painter.setPen(Qt.NoPen)

        rect = QRectF(0, 0, self.width(), self.height())
        anval = self.AnimateVal / 100
        padding = 10
        rect = rect.adjusted(-padding * anval, -padding * anval, padding * anval, padding * anval)
        path.addRoundedRect(rect.adjusted(padding / 2, padding, -padding / 2, -padding), self.Radius, self.Radius)

        painter.drawPath(path)


        if self.AnimateEnabled and self.isHover():
            painter.setBrush(QBrush(QColor(0, 0, 0)))

            painter.setClipPath(path)
            painter.setPen(Qt.black)
            radiusEffect = 75
            path2.addEllipse(self.rect().center(), radiusEffect * anval, radiusEffect * anval)

            painter.drawPath(path2)

        painter.setPen(BaseForeground if not self.isHover() else BaseForegroundHover)
        painter.drawText(self.rect(), Qt.AlignCenter, self.getText())

    _animateVal = 0
    def setAnimateVal(self, val):
        self._animateVal = val
    def getAnimateVal(self):
        return self._animateVal

    AnimateVal = QtCore.Property(int, getAnimateVal, setAnimateVal)

    def Custom_enterEvent(self, event):
        self._m_hover = True
        self.propertyanimation.stop()
        self.propertyanimation.setStartValue(self.getAnimateVal())
        self.propertyanimation.setEndValue(100)
        self.propertyanimation.setEasingCurve(QEasingCurve.InQuad)


        def valChanged():
            self.update()

        def finished():
            self.AnimateVal = 100

        self.propertyanimation.valueChanged.connect(valChanged)
        self.propertyanimation.finished.connect(finished)
        self.propertyanimation.start()
        return QPushButton.enterEvent(self, event)

    def Custom_leaveEvent(self, event) -> None:
        self._m_hover = False
        self.propertyanimation.stop()
        self.propertyanimation.setStartValue(self.getAnimateVal())
        self.propertyanimation.setEndValue(0)
        self.propertyanimation.setEasingCurve(QEasingCurve.OutCubic)

        def valChanged():
            self.update()

        def finished():
            self.AnimateVal = 0

        self.propertyanimation.valueChanged.connect(valChanged)
        self.propertyanimation.finished.connect(finished)
        self.propertyanimation.start()
        return QPushButton.leaveEvent(self, event)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    wind = QMainWindow()
    wind.setStyleSheet("QMainWindow{background-color:rgb(247,247,250)}")
    wind.resize(150, 80)
    wid = QWidget()
    lay = QHBoxLayout(wid)
    lay.setAlignment(Qt.AlignCenter)

    mycustombutton = EButton19()
    mycustombutton.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
    lay.addWidget(mycustombutton)
    wind.setCentralWidget(wid)
    wind.show()
    sys.exit(app.exec())

My code has big bad appearance (if you run it, you will see). I need help! (for 3rd & 19th button)

Thanks!

CodePudding user response:

The main issue with your code is that you're clipping using a float based rectangle, but the painting is still happening in coordinates that are conceptually integers and also ignore the pen width.

You're also making your code more complex than it should, not considering the "overwriting" of enter/leave events that uses differently named methods instead of properly overriding the base implementation.

I would suggest a cleaner approach that, most importantly, considers the proper clipping coordinates, and also uses a single animation instead of continuously creating new ones (which could eventually lead to unnecessary memory usage, since those animations are not destroyed after they are finished.

The important part is that setClipPath() uses the IntersectClip argument, so the clipping will only be set for the result of the merged paths (only using their common areas).

Finally, it's not always necessary to create a custom property for Qt animations: a QVariantAnimation is normally quite sufficient (and efficient, unless you overuse them), since you only access it when needed, instead of calling the Property setter.

Before getting to the proposed solution, some other notes:

  • creating variables that will not be used in a (possibly) very frequently called function is a terrible idea; what is the point on creating a background color for the hover state every time you paint the widget, even when the widget is not hovered at all?
  • local functions (similarly to lambdas) should only be used when they make sense; you either have local functions based on explicitly (and not persistent) local variables, or you properly create methods;
  • apart from very specific situations, if you have animations that almost always do the same thing, you shall not create a new animation anytime you need it, especially if you're not sure about its deletion; otherwise, you could end up with [tens of] thousands of persisting animation objects that occupy memory for nothing.
class Button(QtWidgets.QPushButton):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.backgroundColors = (
            QtGui.QColor(QtCore.Qt.black), 
            QtGui.QColor(QtCore.Qt.lightGray)
        )
        self.foregroundColors = (
            QtGui.QColor(QtCore.Qt.white), 
            QtGui.QColor(QtCore.Qt.black)
        )

        font = self.font()
        font.setBold(True)
        self.setFont(font)

        self.hoverAnimation = QtCore.QVariantAnimation(self)
        # NOTE: both start and end values *must* be floats, otherwise the
        # animation will just "jump" from 0 to 1 and vice versa!
        self.hoverAnimation.setStartValue(0.)
        self.hoverAnimation.setEndValue(1.)
        self.hoverAnimation.setEasingCurve(QtCore.QEasingCurve.OutCubic)
        self.hoverAnimation.setDuration(400)
        self.hoverAnimation.valueChanged.connect(self.update)

    def enterEvent(self, event):
        super().enterEvent(event)
        self.hoverAnimation.setDirection(self.hoverAnimation.Forward)
        self.hoverAnimation.start()

    def leaveEvent(self, event):
        super().leaveEvent(event)
        self.hoverAnimation.setDirection(self.hoverAnimation.Backward)
        self.hoverAnimation.start()

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        qp.setRenderHint(qp.Antialiasing)
        qp.save()

        radius = max(4, min(self.height(), self.width()) * .125)
        clipRect = QtCore.QRectF(self.rect().adjusted(0, 0, -1, -1))
        borderPath = QtGui.QPainterPath()
        borderPath.addRoundedRect(clipRect, radius, radius)
        qp.setClipPath(borderPath)

        qp.fillRect(self.rect(), self.backgroundColors[0])
        qp.setPen(self.foregroundColors[0])
        qp.drawText(self.rect(), 
            QtCore.Qt.AlignCenter|QtCore.Qt.TextShowMnemonic, self.text())

        aniValue = self.hoverAnimation.currentValue()
        if aniValue:
            # use an arbitrary "center" for the radius, based on the widget size
            extent = min(self.height(), self.width()) * 3
            angle = atan(extent / self.width())
            reference = cos(angle) * (extent   self.width())
            x = self.width() - reference
            ratio = 1 - aniValue

            hoverPath = QtGui.QPainterPath()
            hoverPath.moveTo(x, 0)
            hoverPath.lineTo(self.width() - reference * ratio, 0)
            hoverPath.lineTo(self.width(), extent)
            hoverPath.lineTo(x, extent)
            qp.setClipPath(hoverPath, QtCore.Qt.IntersectClip)

            qp.fillRect(self.rect(), self.backgroundColors[1])
            qp.setPen(self.foregroundColors[1])
            qp.drawText(self.rect(), QtCore.Qt.AlignCenter|QtCore.Qt.TextShowMnemonic, self.text())

        qp.restore()
        qp.translate(.5, .5)
        qp.drawRoundedRect(clipRect, radius, radius)

Note: the above implementation is not perfect yet (it has some issues with very wide and short buttons. I'll eventually update this answer as soon as possible, just consider the code as a conceptual reference.

  • Related