本代码是一个组件库中提取出来的,原地址为https://kgithub.com/zhiyiYo/QMaterialWidgets
from enum import Enum
from PyQt5.Qt import *
from qtawesome import icon
class RippleStyle(Enum):
""" Ripple style """
CENTERED = 0
POSITIONED = 1
NONE = 2
class RippleAnimation(QParallelAnimationGroup):
"""
涟漪动画
"""
def __init__(self, center: QPoint, overlay=None, parent=None):
super().__init__(parent=parent)
self._radius = 0
self._opacity = 0
self.center = center
self.overlay = overlay # type: RippleOverlayWidget
self.brush = QBrush(Qt.GlobalColor.black)
self.radiusAni = QPropertyAnimation(self, b'radius', self)
self.opacityAni = QPropertyAnimation(self, b'opacity', self)
self.opacityAni.setStartValue(0.5)
self.opacityAni.setEndValue(0)
self.opacityAni.setDuration(800)
self.opacityAni.setEasingCurve(QEasingCurve.Type.OutQuad)
self.radiusAni.setStartValue(0)
self.radiusAni.setEndValue(300)
self.radiusAni.setDuration(800)
self.radiusAni.setEasingCurve(QEasingCurve.Type.OutQuad)
self.addAnimation(self.radiusAni)
self.addAnimation(self.opacityAni)
def setRadiusEndValue(self, radius: float):
self.radiusAni.setEndValue(radius)
def setRadiusDuration(self, duration: int):
self.radiusAni.setDuration(duration)
def setOpacityStartValue(self, opacity: float):
self.opacityAni.setStartValue(opacity)
def setOpacityDuration(self, duration: int):
self.opacityAni.setDuration(duration)
@pyqtProperty(float)
def radius(self):
return self._radius
@radius.setter
def radius(self, r):
self._radius = r
self.overlay.update()
@pyqtProperty(float)
def opacity(self):
return self._opacity
@opacity.setter
def opacity(self, r):
self._opacity = r
self.overlay.update()
def setColor(self, color: QColor):
if self.brush.color() == color:
return
self.brush.setColor(color)
self.overlay.update()
def setBrush(self, brush: QBrush):
self.brush = brush
self.overlay.update()
def setOverlay(self, overlay):
self.overlay = overlay
class OverlayWidget(QWidget):
""" Overlay widget """
def __init__(self, parent: QWidget):
super().__init__(parent=parent)
parent.installEventFilter(self)
def eventFilter(self, obj: 'QObject', e: 'QEvent'):
if obj is not self.parent():
return super().eventFilter(obj, e)
if e.type() in [QEvent.Type.Move, QEvent.Type.Resize]:
self.setGeometry(self.overlayGeometry())
return super().eventFilter(obj, e)
def overlayGeometry(self):
return self.parentWidget().rect()
class RippleOverlayWidget(OverlayWidget):
""" Ripple overlay widget """
def __init__(self, parent: QWidget):
super().__init__(parent)
self.ripples = []
self.clipPath = QPainterPath()
self.isClipEnabled = True
self.rippleStyle = RippleStyle.POSITIONED
self.rippleOpacityDuration = 1000
self.rippleRadiusDuration = 1400
self.rippleStartOpacity = 0.6
self.triggeredEvent = QEvent.Type.MouseButtonPress
parent.installEventFilter(self)
self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents)
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground)
def setClipEnabled(self, isEnabled: bool):
self.isClipEnabled = isEnabled
self.update()
def setClipPath(self, path):
self.clipPath = path
self.update()
def addRipple(self, ripple):
""" add ripple animation """
ripple.setOverlay(self)
self.ripples.append(ripple)
ripple.finished.connect(lambda: self.removeRipple(ripple))
ripple.start()
def removeRipple(self, ripple):
if ripple not in self.ripples:
return
self.ripples.remove(ripple)
ripple.deleteLater()
self.update()
def eventFilter(self, obj: 'QObject', e: 'QEvent'):
if obj is not self.parent() or not obj.isEnabled() or self.rippleStyle == RippleStyle.NONE or e.type() != self.triggeredEvent:
return super().eventFilter(obj, e)
# add ripple
if self.rippleStyle == RippleStyle.CENTERED:
pos = self._rippleCenter()
radius = self.width() // 2
else:
# QMouseEvent
pos = e.pos()
radius = max(pos.x(), self.width() - pos.x())
ripple = RippleAnimation(pos, self, self)
ripple.setColor(QColor('#aa00ff'))
ripple.setRadiusEndValue(radius)
ripple.setOpacityStartValue(self.rippleStartOpacity)
ripple.setRadiusDuration(self.rippleRadiusDuration)
ripple.setOpacityDuration(self.rippleOpacityDuration)
self.addRipple(ripple)
return super().eventFilter(obj, e)
def _rippleCenter(self):
return self.rect().center()
def paintEvent(self, e):
painter = QPainter(self)
painter.setRenderHints(QPainter.RenderHint.Antialiasing)
painter.setPen(Qt.PenStyle.NoPen)
if self.isClipEnabled:
painter.setClipPath(self.clipPath)
for ripple in self.ripples:
self._drawRipple(painter, ripple)
def _drawRipple(self, painter: QPainter, ripple):
painter.setOpacity(ripple.opacity)
painter.setBrush(ripple.brush)
painter.drawEllipse(ripple.center, ripple.radius, ripple.radius)
class PushButton(QPushButton):
""" push button """
def __init__(self, text: str = '', parent: QWidget = None):
super().__init__(parent)
self.setText(text)
self.rippleWidget = RippleOverlayWidget(self)
self.setBorderRadius(-1)
def setProperty(self, name: str, value) -> bool:
if name != 'icon':
return super().setProperty(name, value)
self.setIcon(value)
return True
def resizeEvent(self, e):
self._updateRippleClipPath()
def _updateRippleClipPath(self):
path = QPainterPath()
path.addRoundedRect(QRectF(self.rect()), self.borderRadius, self.borderRadius)
self.rippleWidget.setClipPath(path)
def getBorderRadius(self):
return self._borderRadius if self._borderRadius >= 0 else self.height() // 2
def setBorderRadius(self, radius: int):
"""
设置边框半径,-1表示全圆角
:param radius:
:return:
"""
self._borderRadius = radius
self._updateRippleClipPath()
self.update()
borderRadius = pyqtProperty(int, getBorderRadius, setBorderRadius)
if __name__ == '__main__':
import sys
QApplication.setHighDpiScaleFactorRoundingPolicy(
Qt.HighDpiScaleFactorRoundingPolicy.PassThrough
)
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps)
app = QApplication(sys.argv)
win = QWidget()
push = PushButton('PushButton', win)
push.setIcon(icon('ei.caret-down'))
push.setIconSize(QSize(30, 30))
push.setText('PushButton')
push.move(100, 100)
push.resize(300, 40)
win.show()
sys.exit(app.exec())