Reputation: 101
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?
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!
Upvotes: 3
Views: 741
Reputation: 48231
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:
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.
Upvotes: 5