实现一个美化QT进度条球体按键

导入

主要是分享一个美化的QT进度条球体按键,因为不复杂,所以没必要做成包,直接设计成模块的形式,哪里需要粘贴到哪里用即可。

大概效果就是这样:

API

主要接口函数有5个:

  • create_circular_button 创建按键

    """
    @brief 创建按键
    @param parent_window,表示依赖的父窗口
    @param callback_func,表示实际执行程序
    @param initial_text,表示按键初始显示文本
    @param theme,球体主题颜色 有'blue'(默认), 'green', 'pink', 'purple'
    @return CircularButton自定义类,继承自Widget
    """
    create_circular_button(parent_window, 
                           callback_func, 
                           initial_text="开始", 
                           theme='blue'):
    
  • start_progress():开启进度条

  • start_auto_progress(duration_ms=8000):开启自动进度,参数时进度条持续时间

  • set_progress_value(i):直接设置进度值 0—100之间

  • finish_progress(): 提前结束进度条

✅Tip: 进度条处理函数有时候就是简单的循环,这样很方便就能插入进度条更新函数,但是很多时候没有循环,执行过程未知或者执行时间未知,这时候就很难插入更新函数。因此给出了2种进度条,一种就是start_progress()手动进度条,配合set_progress_value(i)实时手动设置进度值。还有一种就是start_auto_progress(duration_ms=8000)自动进度条,会持续参数设置的时间,需要配合finish_progress()提前结束,比如我们可以设置一个偏大的时间间隔,然后真正执行完毕之后,提前结束。

完整代码

# circular_button.py
import math
from PyQt5.QtWidgets import QWidget
from PyQt5.QtCore import Qt, QTimer, QRectF
from PyQt5.QtGui import QPainter, QColor, QRadialGradient, QFont, QPainterPath
from PyQt5.QtCore import pyqtSignal

COLOR_THEMES = {
    'blue': {
        'sphere_center': QColor(80, 120, 180, 255),
        'sphere_mid': QColor(40, 80, 140, 255),
        'sphere_edge': QColor(20, 50, 90, 255),
        'wave_normal': QColor(30, 100, 180, 180),
        'wave_hover': QColor(60, 140, 220, 200),
        'wave_progress': QColor(0, 160, 220, 220)
    },
    'green': {
        'sphere_center': QColor(80, 180, 120, 255),
        'sphere_mid': QColor(40, 140, 80, 255),
        'sphere_edge': QColor(20, 90, 50, 255),
        'wave_normal': QColor(30, 180, 100, 180),
        'wave_hover': QColor(60, 220, 140, 200),
        'wave_progress': QColor(0, 220, 160, 220)
    },
    'pink': {
        'sphere_center': QColor(180, 80, 120, 255),
        'sphere_mid': QColor(140, 40, 80, 255),
        'sphere_edge': QColor(90, 20, 50, 255),
        'wave_normal': QColor(180, 30, 100, 180),
        'wave_hover': QColor(220, 60, 140, 200),
        'wave_progress': QColor(220, 0, 160, 220)
    },
    'purple': {
        'sphere_center': QColor(120, 80, 180, 255),
        'sphere_mid': QColor(80, 40, 140, 255),
        'sphere_edge': QColor(50, 20, 90, 255),
        'wave_normal': QColor(100, 30, 180, 180),
        'wave_hover': QColor(140, 60, 220, 200),
        'wave_progress': QColor(160, 0, 220, 220)
    }
}


class WaveEffect:
    """波浪效果类,用于生成动态波浪""" 
    def __init__(self):
        self.offset = 0                    # 波浪偏移量,用于动画
        self.amplitude = 4                 # 波浪振幅(高度)
        self.wavelength = 35               # 波浪波长
        self.speed = 1                  # 波浪移动速度
        
    def update(self):
        """更新波浪偏移量,实现从左向右的波动效果"""
        self.offset += self.speed
        if self.offset > self.wavelength:
            self.offset = 0
            
    def get_wave_points(self, rect, water_level):
        """
        生成波浪路径点
        
        Args:
            rect: 绘制区域矩形
            water_level: 水位高度比例 (0.0 - 1.0),0=底部,1=顶部
            
        Returns:
            波浪点坐标列表 [(x, y), ...]
        """
        points = []
        width = rect.width()
        height = rect.height()
        center_x = rect.center().x()
        center_y = rect.center().y()
        
        # 计算水位对应的y坐标(从底部开始)
        # 注意:water_level=0 表示水在最底部,water_level=1 表示水充满整个区域
        water_y = center_y + (height / 2) - (height * water_level)
        
        # 生成波浪点,步长为2像素
        start_x = int(center_x - width/2)
        end_x = int(center_x + width/2 + 1)
        
        for x in range(start_x, end_x, 2):
            # 正弦波公式:y = 水位 + 振幅 * sin(2π * (x - 偏移) / 波长)
            wave_height = self.amplitude * math.sin(2 * math.pi * (x - self.offset) / self.wavelength)
            y = water_y + wave_height
            points.append((x, y))
            
        return points


class CircularButton(QWidget):
    """自定义圆形按钮,具有立体效果、波浪动画和进度显示功能"""
    
    # 自定义点击信号
    clicked = pyqtSignal()
    
    def __init__(self, parent=None, theme='blue'):
        super().__init__(parent)
        self.setFixedSize(200, 200)        # 固定按钮大小
        self.setMouseTracking(True)        # 启用鼠标跟踪,用于悬停效果
        
        # 状态变量
        self._hovered = False              # 鼠标是否悬停
        self._pressed = False              # 是否被按下
        self._progress = 0                 # 进度值 (0-100)
        self._operation_running = False    # 是否正在执行操作
        
        # 波浪效果实例
        self.wave_effect = WaveEffect()
        
        # 波浪动画定时器(20FPS)
        self.wave_timer = QTimer(self)
        self.wave_timer.timeout.connect(self.update_wave)
        self.wave_timer.start(50)
      
        self._current_theme = theme
        self._apply_theme(theme)

        self._initial_text = "开始"
        # 自动进度相关
        self._auto_progress_timer = None

    def _apply_theme(self, theme_name):
        """应用颜色主题"""
        theme = COLOR_THEMES.get(theme_name, COLOR_THEMES['blue'])
        self.normal_wave_color = theme['wave_normal']
        self.hover_wave_color = theme['wave_hover']
        self.progress_wave_color = theme['wave_progress']
        self.sphere_colors = {
            'center': theme['sphere_center'],
            'mid': theme['sphere_mid'],
            'edge': theme['sphere_edge']
        }

    def set_initial_text(self, text):
        """设置按钮初始显示文本"""
        self._initial_text = text
        self.update()
    
    def update_wave(self):
        """更新波浪效果并触发重绘"""
        # 只有在没有待重置操作时才更新波浪
        self.wave_effect.update()
        self.update()

    def start_progress(self):
        """开始进度模式(隐藏初始文本,显示波浪)"""
        self._operation_running = True
        self._progress = 0
        self.update()

    def start_auto_progress(self, duration_ms=3000):
        """开始自动增长进度(适用于无法手动更新进度的场景)"""
        self._operation_running = True
        self._progress = 0
        self.update()
        
        # 计算步长和间隔
        total_steps = 100
        interval = duration_ms // total_steps
        
        self._auto_progress_timer = QTimer(self)
        self._auto_progress_timer.timeout.connect(self._auto_progress_step)
        self._auto_progress_timer.start(interval)

    def set_progress_value(self, value):
        """设置具体进度值 (0-100)"""
        if self._operation_running:
            self._progress = max(0, min(100, value))
            self.update()
            if self._progress >= 100:
                self._operation_running = False
                self._progress = 0


    def _auto_progress_step(self):
        """自动进度步进"""
        self._progress += 1
        if self._progress >= 100:
            self._progress = 100
            self._auto_progress_timer.stop()
            self._auto_progress_timer = None
            self._operation_running = False
        self.update()

    def finish_progress(self):
        """直接完成进度到100%"""
        if self._auto_progress_timer:
            self._auto_progress_timer.stop()
            self._auto_progress_timer = None
        self.set_progress_value(100)
    
    def paintEvent(self, event):
        """绘制事件,负责绘制整个按钮"""
        painter = QPainter(self)
        painter.setRenderHint(QPainter.Antialiasing)      # 启用抗锯齿
        painter.setRenderHint(QPainter.SmoothPixmapTransform)  # 启用平滑变换
        
        rect = self.rect()
        center = rect.center()
        radius = min(rect.width(), rect.height()) // 2 - 2  # 内部绘制半径
        
        # 绘制立体球体效果(在圆形内部)
        self.draw_sphere_effect(painter, center, radius)
        
        # 绘制内部内容(波浪或进度)
        if self._operation_running:
            self.draw_progress_water(painter, center, radius)
        else:
            self.draw_wave_effect(painter, center, radius)
    
    def draw_sphere_effect(self, painter, center, radius):
        """绘制内部立体球体效果"""
        # 创建径向渐变模拟球体光照
        radial_grad = QRadialGradient(center, radius * 1.2, center)
        # 中心亮色
        radial_grad.setColorAt(0.0, self.sphere_colors['center'])
        # 中间过渡色
        radial_grad.setColorAt(0.6, self.sphere_colors['mid'])
        # 边缘深色
        radial_grad.setColorAt(1.0, self.sphere_colors['edge'])
        
        # 绘制主圆形背景
        painter.setBrush(radial_grad)
        painter.setPen(Qt.NoPen)
        painter.drawEllipse(int(center.x() - radius), int(center.y() - radius), 
                           int(radius * 2), int(radius * 2))
        
        # 绘制高光效果(增强立体感)
        highlight_x = int(center.x() - radius * 0.3)
        highlight_y = int(center.y() - radius * 0.3)
        highlight_radius = int(radius * 0.2)
        
        highlight_grad = QRadialGradient(highlight_x, highlight_y, highlight_radius, highlight_x, highlight_y)
        highlight_grad.setColorAt(0.0, QColor(255, 255, 255, 120))
        highlight_grad.setColorAt(1.0, QColor(255, 255, 255, 0))
        
        painter.setBrush(highlight_grad)
        painter.drawEllipse(highlight_x - highlight_radius, highlight_y - highlight_radius,
                           highlight_radius * 2, highlight_radius * 2)
    
    def draw_wave_effect(self, painter, center, radius):
        """绘制波浪效果(被圆形裁剪)"""
        # 根据悬停状态选择波浪颜色
        wave_color = self.hover_wave_color if self._hovered else self.normal_wave_color
        
        # 创建内部绘制区域(留出边框空间)
        inner_radius = radius - 8
        wave_rect = QRectF(
            center.x() - inner_radius,
            center.y() - inner_radius,
            inner_radius * 2,
            inner_radius * 2
        )
        
        # 创建圆形裁剪路径,确保波浪不超出圆形范围
        clip_path = QPainterPath()
        clip_path.addEllipse(center, inner_radius, inner_radius)
        painter.setClipPath(clip_path)
        
        # 生成波浪点(固定水位30%)
        points = self.wave_effect.get_wave_points(wave_rect, 0.3)
        if points:
            from PyQt5.QtGui import QPolygonF
            from PyQt5.QtCore import QPointF
            
            # 构建波浪多边形
            polygon_points = []
            # 添加波浪上边缘点
            for x, y in points:
                polygon_points.append(QPointF(x, y))
            
            # 添加底部点(从右到左闭合多边形)
            right_x = points[-1][0] if points else wave_rect.right()
            left_x = points[0][0] if points else wave_rect.left()
            bottom_y = wave_rect.bottom()
            
            polygon_points.append(QPointF(right_x, bottom_y))
            polygon_points.append(QPointF(left_x, bottom_y))
            
            wave_polygon = QPolygonF(polygon_points)
            
            # 绘制波浪
            painter.setBrush(wave_color)
            painter.setPen(Qt.NoPen)
            painter.drawPolygon(wave_polygon)
        
        # 绘制初始文本
        if not self._operation_running:
            painter.setClipping(False)  # 先重置裁剪以便绘制文本
            painter.setPen(QColor(255, 255, 255))
            painter.setFont(QFont("Arial", 14, QFont.Bold))
            text_rect = painter.boundingRect(self.rect(), Qt.AlignCenter, self._initial_text)
            painter.drawText(text_rect, Qt.AlignCenter, self._initial_text)
        # 重置裁剪区域
        painter.setClipping(False)
    
    def draw_progress_water(self, painter, center, radius):
        """绘制进度水面上涨效果(被圆形裁剪)"""
        # 计算当前水位(0-1)
        water_level = self._progress / 100.0
        
        # 创建内部绘制区域(留出边框空间)
        inner_radius = radius - 8
        water_rect = QRectF(
            center.x() - inner_radius,
            center.y() - inner_radius,
            inner_radius * 2,
            inner_radius * 2
        )
        
        # 创建圆形裁剪路径,确保波浪不超出圆形范围
        clip_path = QPainterPath()
        clip_path.addEllipse(center, inner_radius, inner_radius)
        painter.setClipPath(clip_path)
        
        # 绘制水面填充
        if water_level > 0:
            points = self.wave_effect.get_wave_points(water_rect, water_level)
            if points:
                from PyQt5.QtGui import QPolygonF
                from PyQt5.QtCore import QPointF
                
                # 构建水面多边形
                polygon_points = []
                for x, y in points:
                    polygon_points.append(QPointF(x, y))
                
                # 添加底部点闭合多边形
                # 底部应该是圆形的底部,而不是矩形的底部
                right_x = points[-1][0] if points else water_rect.right()
                left_x = points[0][0] if points else water_rect.left()
                bottom_y = center.y() + inner_radius
                
                polygon_points.append(QPointF(right_x, bottom_y))
                polygon_points.append(QPointF(left_x, bottom_y))
                
                water_polygon = QPolygonF(polygon_points)
                
                # 绘制水面
                painter.setBrush(self.progress_wave_color)
                painter.setPen(Qt.NoPen)
                painter.drawPolygon(water_polygon)
        
        # 绘制进度文本
        if self._progress > 0:
            painter.setPen(QColor(255, 255, 255))
            painter.setFont(QFont("Arial", 16, QFont.Bold))
            progress_text = f"{self._progress}%"
            text_rect = painter.boundingRect(self.rect(), Qt.AlignCenter, progress_text)
            painter.drawText(text_rect, Qt.AlignCenter, progress_text)
        
        # 重置裁剪区域
        painter.setClipping(False)
    
    def enterEvent(self, event):
        """鼠标进入事件"""
        self._hovered = True
        self.update()
        super().enterEvent(event)
    
    def leaveEvent(self, event):
        """鼠标离开事件"""
        self._hovered = False
        self.update()
        super().leaveEvent(event)
    
    def mousePressEvent(self, event):
        """鼠标按下事件"""
        if event.button() == Qt.LeftButton:
            self._pressed = True
            self.update()
        super().mousePressEvent(event)
    
    def mouseReleaseEvent(self, event):
        """鼠标释放事件"""
        if event.button() == Qt.LeftButton and self._pressed:
            self._pressed = False
            self.clicked.emit()
        super().mouseReleaseEvent(event)


def _wrapped_callback(callback_func, button):
    """包装回调函数,确保按钮状态正确"""
    callback_func()


# 接口API函数
def create_circular_button(parent_window, callback_func, initial_text="开始", theme='blue'):
    """
    创建圆形波浪按钮的公共API接口
    
    Args:
        parent_window: 父窗口对象(QWidget)
        callback_func: 回调函数,当按钮点击时执行真正的操作
        initial_text: 按钮初始显示的文本(默认为"开始")
        theme: 颜色主题,可选 'blue', 'green', 'pink', 'purple'(默认'blue')
    Returns:
        CircularButton实例,可直接添加到布局中使用
    """
    button = CircularButton(parent_window, theme=theme)
    button.set_initial_text(initial_text)
    button.clicked.connect(lambda: _wrapped_callback(callback_func, button))
    return button

例子:

测试代码:

# demo.py
import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout
from PyQt5.QtCore import Qt
from circular_button import create_circular_button
import time


class TestWindow(QWidget):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("圆形按钮测试")
        self.setGeometry(300, 300, 300, 300)
        
        layout = QVBoxLayout()
        layout.setAlignment(Qt.AlignCenter)
        
        # 使用API创建按钮
        self.button1 = create_circular_button(
            parent_window=self,
            callback_func=self.my_real_operation,
            initial_text="点击开始",
            theme='green'
        )        
        layout.addWidget(self.button1)

        self.button2 = create_circular_button(
            parent_window=self,
            callback_func=self.my_real_operation2,
            initial_text="点击开始",
            theme='pink'
        )    
        layout.addWidget(self.button2)
        self.setLayout(layout)
    
    def my_real_operation(self):
        """真正的执行函数"""
        self.button1.start_progress()
       
        # 模拟耗时操作并更新进度
        for i in range(101):
            time.sleep(0.05)  # 模拟工作      
            # 更新进度
            self.button1.set_progress_value(i)         
            # 处理Qt事件队列,保持界面响应
            QApplication.processEvents()

    def my_real_operation2(self):
        """真正的执行函数"""
        self.button2.start_auto_progress(duration_ms=8000)     
        # 模拟耗时操作并更新进度
        for i in range(101):
            # 执行实际工作...
            time.sleep(0.05)  # 模拟工作            
            # 处理Qt事件队列,保持界面响应
            QApplication.processEvents()
        self.button2.finish_progress()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = TestWindow()
    window.show()
    sys.exit(app.exec_())


效果:

模块使用

拿我以前写的一个windows 垃圾清理程序为例:

非常简单的界面,现在我们把清理按键换成带进度条的球体按键,原来按键的创建是这样:

        self.clean_btn = QPushButton("开始清理")
        self.clean_btn.setObjectName("cleanBtn")
        self.clean_btn.clicked.connect(self.start_cleaning)
        self.clean_btn.setEnabled(False)

我们替换成:

self.clean_btn = create_circular_button(
            parent_window=self,
            callback_func=self.start_cleaning,
            initial_text="开始清理"
        )

具体的清理函数时在一个clean worker线程中,我们使用自动进度条,直接添加:

# 在新线程中清理
        def clean_worker():
            self.clean_btn.start_auto_progress(duration_ms=5000) 
         # 其他清理代码

然后再清理完成的后续处理函数中,加上self.clean_btn.finish_progress()即可。我们看下效果:

  

整体的模块使用还是很方便的,可以方便嵌入到自己的QT UI之中。

其他

根据这个模块代码,可以自由进行修改,我并没有再API中添加过多的参数,实际使用时候,有的就直接再模块中修改了。如果想要这个模块更加灵活,可以把参数移到API的参数中,没有什么难度。比如这个按钮时固定大小的,你可以把大小也放到API参数中设置,这个更建议,因为一般都是要修改的。以及如果想要更加美化的话,可以添加图片背景配合光影效果,甚至可以在波浪水面上添加粒子效果,也都并不复杂。