You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PySide6复刻tympanus两款自定义动画按钮效果求助

PySide6 自定义悬停动画按钮修正方案

核心问题汇总

  • 3号斜切扫动按钮
    • 斜边形状计算错误:原代码误用角度传入弧度制三角函数,生成的多边形不符合原设计的固定斜角扫动效果
    • 文字颜色无分层:原设计扫过区域文字为黑色、未扫过区域为白色,现有实现为全局统一文字色
    • 动画对象管理混乱:每次进出事件都新建动画对象、重复绑定信号,易造成内存泄漏和动画冲突
  • 19号圆形扩散按钮
    • 扩散起点错误:原设计圆形从鼠标进入按钮的位置开始扩散,现有实现固定从按钮中心扩散
    • 边框动效逻辑错误:原设计悬停时边框向外扩张并渐隐,现有实现为内缩调整
    • 同样存在文字分层缺失、动画信号重复绑定问题

修正后完整代码

3号斜切扫动按钮(EButton3)

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

class EButton3(QPushButton):
    Radius = 10
    _m_Text = ""
    _m_hover = False
    _animateVal = 0

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setText("Button")
        # 复用同一个动画对象,避免重复创建
        self._ani = QPropertyAnimation(self, b'AnimateVal', self)
        self._ani.setDuration(450)
        self._ani.valueChanged.connect(self.update)

    def getText(self):
        return self._m_Text

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

    def isHover(self):
        return self._m_hover

    @Property(int)
    def AnimateVal(self):
        return self._animateVal

    @AnimateVal.setter
    def AnimateVal(self, val):
        self._animateVal = val

    def paintEvent(self, event: QPaintEvent):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setPen(Qt.NoPen)

        # 绘制基础黑色背景
        bg_path = QPainterPath()
        base_rect = QRectF(0, 0, self.width(), self.height())
        bg_path.addRoundedRect(base_rect, self.Radius, self.Radius)
        painter.setBrush(QColor(Qt.black))
        painter.drawPath(bg_path)

        anval = self.AnimateVal / 100
        if self.AnimateVal > 0:
            # 固定斜角斜率,生成扫动的平行四边形
            skew_offset = self.height() * 0.6
            sweep_poly = QPolygonF([
                QPointF(self.width() * anval + skew_offset, 0),
                QPointF(self.width() * anval - skew_offset, self.height()),
                QPointF(-skew_offset*2, self.height()),
                QPointF(-skew_offset*2, 0)
            ])
            sweep_path = QPainterPath()
            sweep_path.addPolygon(sweep_poly)
            # 裁剪在按钮圆角范围内
            painter.setClipPath(bg_path)
            painter.setBrush(QColor(231, 231, 231))
            painter.drawPath(sweep_path)

            # 分层绘制文字:扫过区域黑色,未扫过区域白色
            painter.setClipping(False)
            # 先画全按钮白色文字
            painter.setPen(QColor(Qt.white))
            painter.drawText(self.rect(), Qt.AlignCenter, self.getText())
            # 裁剪到扫动区域画黑色文字
            painter.setClipPath(sweep_path.intersected(bg_path))
            painter.setPen(QColor(Qt.black))
            painter.drawText(self.rect(), Qt.AlignCenter, self.getText())
        else:
            # 未触发动画时直接画白色文字
            painter.setPen(QColor(Qt.white))
            painter.drawText(self.rect(), Qt.AlignCenter, self.getText())

    def enterEvent(self, event) -> None:
        self._m_hover = True
        self._ani.stop()
        self._ani.setStartValue(self._animateVal)
        self._ani.setEndValue(100)
        self._ani.setEasingCurve(QEasingCurve.OutCubic)
        self._ani.start()
        super().enterEvent(event)

    def leaveEvent(self, event) -> None:
        self._m_hover = False
        self._ani.stop()
        self._ani.setStartValue(self._animateVal)
        self._ani.setEndValue(0)
        self._ani.setEasingCurve(QEasingCurve.InCubic)
        self._ani.start()
        super().leaveEvent(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())

19号圆形扩散按钮(EButton19)

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

class EButton19(QPushButton):
    Radius = 10
    _m_Text = ""
    _m_hover = False
    _animateVal = 0
    _hover_pos = QPointF()

    def __init__(self, parent=None):
        super().__init__(parent)
        self.setText("Button")
        self._ani = QPropertyAnimation(self, b'AnimateVal', self)
        self._ani.setDuration(350)
        self._ani.valueChanged.connect(self.update)

    def getText(self):
        return self._m_Text

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

    def isHover(self):
        return self._m_hover

    @Property(int)
    def AnimateVal(self):
        return self._animateVal

    @AnimateVal.setter
    def AnimateVal(self, val):
        self._animateVal = val

    def paintEvent(self, event: QPaintEvent):
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        painter.setPen(Qt.NoPen)

        base_rect = QRectF(0, 0, self.width(), self.height())
        bg_path = QPainterPath()
        bg_path.addRoundedRect(base_rect, self.Radius, self.Radius)
        # 绘制浅灰色基础背景
        painter.setBrush(QColor(231,231,231))
        painter.drawPath(bg_path)

        anval = self._animateVal / 100
        # 计算扩散半径,取到按钮四个角的最大距离保证完全覆盖
        max_radius = math.hypot(max(self._hover_pos.x(), self.width()-self._hover_pos.x()),
                                max(self._hover_pos.y(), self.height()-self._hover_pos.y()))
        if anval > 0:
            # 绘制扩散圆形
            painter.setClipPath(bg_path)
            painter.setBrush(QColor(Qt.black))
            painter.drawEllipse(self._hover_pos, max_radius * anval, max_radius * anval)
            painter.setClipping(False)

            # 绘制外扩渐隐边框
            border_pad = 10 * anval
            border_path = QPainterPath()
            border_rect = base_rect.adjusted(-border_pad, -border_pad, border_pad, border_pad)
            border_path.addRoundedRect(border_rect, self.Radius + border_pad, self.Radius + border_pad)
            painter.setPen(QPen(QColor(0,0,0, 255*(1-anval)), 2))
            painter.setBrush(Qt.NoBrush)
            painter.drawPath(border_path)
            painter.setPen(Qt.NoPen)

            # 分层绘制文字
            painter.setClipping(False)
            painter.setPen(QColor(Qt.black))
            painter.drawText(self.rect(), Qt.AlignCenter, self.getText())
            painter.setClipPath(bg_path.intersected(QPainterPath().addEllipse(self._hover_pos, max_radius * anval, max_radius * anval)))
            painter.setPen(QColor(Qt.white))
            painter.drawText(self.rect(), Qt.AlignCenter, self.getText())
        else:
            # 初始状态黑色文字
            painter.setPen(QColor(Qt.black))
            painter.drawText(self.rect(), Qt.AlignCenter, self.getText())

    def enterEvent(self, event):
        self._m_hover = True
        # 记录鼠标进入的位置作为扩散起点
        self._hover_pos = event.position()
        self._ani.stop()
        self._ani.setStartValue(self._animateVal)
        self._ani.setEndValue(100)
        self._ani.setEasingCurve(QEasingCurve.OutQuad)
        self._ani.start()
        super().enterEvent(event)

    def leaveEvent(self, event) -> None:
        self._m_hover = False
        self._ani.stop()
        self._ani.setStartValue(self._animateVal)
        self._ani.setEndValue(0)
        self._ani.setEasingCurve(QEasingCurve.InQuad)
        self._ani.start()
        super().leaveEvent(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())

效果说明

  • 修正后的3号按钮实现了固定斜角的扫动效果,文字随扫动区域自动切换颜色,动画平滑无冲突
  • 修正后的19号按钮从鼠标进入位置开始圆形扩散,外边框同步向外扩张渐隐,文字颜色随填充区域切换
  • 两个按钮均复用动画对象,避免了重复创建对象、重复绑定信号导致的内存泄漏问题,动画进出衔接自然

内容的提问来源于stack exchange,提问作者Dream59

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 13:27:19