导入
主要是分享一个美化的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参数中设置,这个更建议,因为一般都是要修改的。以及如果想要更加美化的话,可以添加图片背景配合光影效果,甚至可以在波浪水面上添加粒子效果,也都并不复杂。