Qt实现等待动画WaitingSpinner(PyQt5)

效果预览

PixPin_2026-05-07_10-40-21

代码

from functools import wraps

from PyQt5.QtWidgets import QWidget, QMessageBox
from PyQt5.QtCore import QTimer, Qt, QRectF, QThread, QEvent, pyqtSignal
from PyQt5.QtGui import QPainter, QColor, QPen


class WaitingSpinner(QWidget):
    # 定义显示模式
    STYLE_DOTS = "dots"       # 走点模式(iOS 风格圆点)
    STYLE_LINE = "line"       # 线条模式(经典 Spinner)
    STYLE_ARC = "arc"         # 弧形模式(Material Design 风格)

    def __init__(self, parent=None, style="dots", color=QColor("#007AFF")):
        super().__init__(parent)  # 必须传入 parent
        self.style_mode = style
        self.color = color
        self.angle = 0
        self.timer = QTimer(self)
        self.timer.timeout.connect(self._update_animation)

        # 初始状态
        self.hide()

        # 监听父控件的事件,确保大小同步
        if parent:
            parent.installEventFilter(self)

    def eventFilter(self, obj, event):
        # 当父控件大小改变时,自动跟随
        if obj == self.parent() and event.type() == QEvent.Resize:
            self.resize(obj.size())
        return super().eventFilter(obj, event)

    def start(self):
        if self.parent():
            self.resize(self.parent().size())  # 覆盖整个父控件
        self.show()
        self.raise_()  # 确保在最顶层
        self.timer.start(60)

    def stop(self):
        self.timer.stop()
        self.hide()

    def _update_animation(self):
        self.angle = (self.angle + 30) % 360 if self.style_mode != self.STYLE_ARC else (self.angle + 10) % 360
        self.update()

    def set_style(self, style):
        self.style_mode = style
        self.update()

    def paintEvent(self, event):
        """绘制事件"""
        super().paintEvent(event)
        # 创建 QPainter 对象
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)
        # 1. 绘制半透明遮罩背景 (可选,增加磨砂感)
        painter.fillRect(self.rect(), QColor(255, 255, 255, 150))
        # 2. 绘制动画 (移至几何中心)
        painter.translate(self.width() / 2, self.height() / 2)
        # 调用指定的绘制逻辑
        if self.style_mode == self.STYLE_DOTS:
            self._draw_dots(painter)
        elif self.style_mode == self.STYLE_LINE:
            self._draw_lines(painter)
        elif self.style_mode == self.STYLE_ARC:
            self._draw_arc(painter)

    def _draw_dots(self, painter):
        """绘制 iOS 风格的圆点旋转"""
        for i in range(8):
            painter.save()
            painter.rotate(self.angle + i * 45)
            # 根据索引设置透明度,形成旋转感
            opacity = int(255 * (i / 8))
            color = QColor(self.color)
            color.setAlpha(opacity)
            painter.setBrush(color)
            painter.setPen(Qt.NoPen)
            painter.drawEllipse(10, -3, 6, 6)  # 在半径 10 的位置画圆
            painter.restore()

    def _draw_lines(self, painter):
        """绘制线条旋转"""
        pen = QPen(self.color)
        pen.setWidth(3)
        pen.setCapStyle(Qt.RoundCap)
        for i in range(12):
            painter.save()
            painter.rotate(self.angle + i * 30)
            opacity = int(255 * (i / 12))
            pen.setColor(QColor(self.color.red(), self.color.green(), self.color.blue(), opacity))
            painter.setPen(pen)
            painter.drawLine(10, 0, 18, 0)
            painter.restore()

    def _draw_arc(self, painter):
        """绘制弧形旋转"""
        pen = QPen(self.color)
        pen.setWidth(4)
        pen.setCapStyle(Qt.RoundCap)
        painter.setPen(pen)
        # 绘制一个 270 度的弧线
        rect = QRectF(-15, -15, 30, 30)
        painter.rotate(self.angle * 2)  # 弧形旋转快一点
        painter.drawArc(rect, 0, 270 * 16)

    # 为静态方法的装饰器
    @staticmethod
    def with_loading(spinner_attr_name="spinner"):
        """
        静态装饰器:执行耗时任务时显示 Spinner

        :param spinner_attr_name: 类实例中 spinner 对象的变量名
        """

        class __Worker(QThread):
            """通用的工作线程,用于执行装饰的函数"""
            finished_signal = pyqtSignal(object)
            error_signal = pyqtSignal(Exception)  # 专门用于传递异常

            def __init__(self, func, *args, **kwargs):
                super().__init__()
                self.func = func
                self.args = args
                self.kwargs = kwargs

            def run(self):
                try:
                    # 执行业务逻辑
                    result = self.func(*self.args, **self.kwargs)
                    self.finished_signal.emit(result)
                except Exception as e:
                    # 捕获所有异常并发送错误信号
                    self.error_signal.emit(e)

        def decorator(func):
            @wraps(func)
            def wrapper(instance, *args, **kwargs):
                # 运行时通过instance(即self)获取spinner
                # 获取类实例中的 spinner 对象
                spinner = getattr(instance, spinner_attr_name, None)
                if spinner:
                    spinner.start()
                # 加载时禁用窗口,防止二次点击
                instance.setEnabled(False)
                # 创建并启动线程
                # 这里的__Worker类需要能在模块作用域找到,或者定义在内部
                instance._worker = __Worker(func, instance, *args, **kwargs)
                # 直接传递 instance, *args, **kwargs
                # 此时 args 里面已经包含了除了 self(instance) 以外的所有位置参数
                # 我们只需要确保 Worker 启动时能够正确调用它们

                # 定义统一的清理逻辑
                def cleanup():
                    if spinner:
                        spinner.stop()
                    instance.setEnabled(True)  # 恢复窗口可点击
                    instance._worker = None  # 显式释放引用

                # 正常完成的回调
                def handle_success(result):
                    cleanup()
                    # 这里可以根据需要处理返回值
                    print(f"任务完成,结果: {result}")
                # 发生错误的回调

                def handle_error(e):
                    cleanup()
                    # 可以在这里弹出错误提示框
                    QMessageBox.critical(instance, "错误", f"任务执行过程中发生异常:\n{str(e)}")

                instance._worker.finished_signal.connect(handle_success)
                instance._worker.error_signal.connect(handle_error)
                instance._worker.start()

            return wrapper
        return decorator


class __MyApp(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("装饰器加载演示")
        # self.setFixedSize(300, 200)

        from PyQt5.QtCore import Qt
        from PyQt5.QtWidgets import QVBoxLayout, QPushButton

        layout = QVBoxLayout(self)

        # 1. 准备 Spinner
        # 创建浮层 Spinner,但不放进 layout
        # 它会自动附着在 self (Demo 窗口) 上
        self.loading_overlay = WaitingSpinner(self, style="dots")

        self.btn = QPushButton("开始执行耗时任务")
        self.btn.clicked.connect(lambda: self.heavy_task(3))
        layout.addWidget(self.btn)

    # 2. 使用装饰器 (指向 self.loading_overlay)
    @WaitingSpinner.with_loading(spinner_attr_name="loading_overlay")
    def heavy_task(self, count: int):
        import time
        # 模拟耗时操作,如从服务器下载数据
        print("工作线程启动,执行次数: ", count)
        for i in range(count):
            time.sleep(3)
            print(f"工作线程进度: {i + 1}/5")
            if i == 1:
                raise Exception("模拟任务失败")
        return "Data Downloaded"


if __name__ == "__main__":
    import sys
    from PyQt5.QtWidgets import QApplication, QWidget
    app = QApplication(sys.argv)
    window = __MyApp()
    window.show()
    sys.exit(app.exec_())
posted @ 2026-05-07 10:40  乌合之众  阅读(24)  评论(0)    收藏  举报
clear