1+ # -*- coding: utf-8 -*-
2+ from PyQt5 .QtWidgets import QPushButton
3+ from PyQt5 .QtSvg import QSvgWidget
4+ from PyQt5 .QtCore import Qt , QSize , QPropertyAnimation , QEasingCurve , QRect
5+ from PyQt5 .QtGui import QColor , QPainter , QPen , QBrush
6+
7+ class MyRoundButton (QPushButton ):
8+ """圆形 SVG 图标按钮
9+
10+ 一个可以显示 SVG 图标的圆形按钮,支持悬停效果和点击效果
11+ """
12+
13+ def __init__ (self , parent = None , svg_path = "" , size = (64 , 64 ), tooltip = "" ,
14+ svg_size = None ,
15+ border_width = 1 , border_color = QColor (255 , 255 , 255 , 0 ),
16+ svg_color = QColor (255 , 255 , 255 , 255 ),
17+ margin = (0 , 0 , 0 , 0 ),
18+ padding = (0 , 0 , 0 , 0 )):
19+ """初始化圆形 SVG 按钮
20+
21+ Args:
22+ parent: 父控件
23+ svg_path: SVG 文件路径
24+ size: 按钮大小,默认为 (64, 64)
25+ tooltip: 鼠标悬停提示文本
26+ svg_size: SVG 图标大小,默认与按钮大小相同
27+ border_width: 边框宽度,默认为 1
28+ border_color: 边框颜色,默认为透明
29+ svg_color: SVG 图标颜色,默认为白色
30+ margin: SVG 图标外边距,格式为 (左, 上, 右, 下),默认为 (0, 0, 0, 0)
31+ padding: SVG 图标内边距,格式为 (左, 上, 右, 下),默认为 (0, 0, 0, 0)
32+ """
33+ super ().__init__ (parent )
34+
35+ # 设置按钮属性
36+ self .setFixedSize (* size )
37+ self .setToolTip (tooltip )
38+ self .setCursor (Qt .PointingHandCursor )
39+
40+ # 设置样式
41+ self .setStyleSheet ("""
42+ QPushButton {
43+ background-color: transparent;
44+ border: none;
45+ }
46+ QPushButton:hover {
47+ background-color: transparent;
48+ }
49+ QPushButton:pressed {
50+ background-color: transparent;
51+ }
52+ """ )
53+
54+ # 创建 SVG 控件
55+ self .svg_widget = QSvgWidget (self )
56+
57+ # 加载 SVG 文件
58+ self .svg_widget .load (svg_path )
59+
60+ # 设置 SVG 控件大小和位置
61+ self .svg_size = svg_size if svg_size is not None else size
62+ self .margin = margin # 设置外边距 (左, 上, 右, 下)
63+ self .padding = padding # 设置内边距 (左, 上, 右, 下)
64+ self .centerSvg () # 居中 SVG 图标
65+
66+ self .setSvgColor (svg_color )
67+
68+ # 设置圆形边框属性
69+ self .border_color = border_color
70+ self .border_width = border_width
71+
72+ # 添加状态跟踪
73+ self .is_hovered = False
74+ self .is_pressed = False
75+
76+ # 设置背景颜色(默认透明)
77+ self .bg_color = QColor (255 , 255 , 255 , 0 )
78+
79+ def centerSvg (self ):
80+ """居中显示 SVG 图标,考虑边距和内边距设置
81+
82+ 当 padding 值为负值时,不做压缩而是直接在对应的位置裁剪
83+ """
84+ # 计算 SVG 控件的位置,使其在按钮中居中
85+ button_width , button_height = self .width (), self .height ()
86+ svg_width , svg_height = self .svg_size
87+ left_margin , top_margin , right_margin , bottom_margin = self .margin
88+ left_padding , top_padding , right_padding , bottom_padding = self .padding
89+
90+ # 计算可用空间(考虑外边距)
91+ available_width = button_width - left_margin - right_margin
92+ available_height = button_height - top_margin - bottom_margin
93+
94+ # 处理 padding 值
95+ # 对于正值 padding,减小 SVG 显示大小
96+ # 对于负值 padding,不减小 SVG 显示大小,而是在后续步骤中通过位置调整实现裁剪效果
97+ width_reduction = max (0 , left_padding ) + max (0 , right_padding )
98+ height_reduction = max (0 , top_padding ) + max (0 , bottom_padding )
99+
100+ # 计算实际 SVG 显示大小
101+ actual_svg_width = min (svg_width - width_reduction , available_width )
102+ actual_svg_height = min (svg_height - height_reduction , available_height )
103+
104+ # 确保 SVG 尺寸不为负值
105+ actual_svg_width = max (actual_svg_width , 0 )
106+ actual_svg_height = max (actual_svg_height , 0 )
107+
108+ # 计算基础居中位置(考虑外边距)
109+ base_x = left_margin + (available_width - actual_svg_width ) / 2
110+ base_y = top_margin + (available_height - actual_svg_height ) / 2
111+
112+ # 应用负值 padding 的裁剪效果
113+ # 负值的左/上 padding 会使 SVG 向左/上移动,实现左/上裁剪
114+ # 负值的右/下 padding 不影响位置,但会在后续步骤中扩大 SVG 尺寸
115+ x_offset = min (0 , left_padding ) # 负值左 padding 导致向左偏移
116+ y_offset = min (0 , top_padding ) # 负值上 padding 导致向上偏移
117+
118+ # 计算最终位置(应用偏移)
119+ final_x = base_x + x_offset
120+ final_y = base_y + y_offset
121+
122+ # 计算最终尺寸(考虑负值 padding 导致的尺寸扩大)
123+ # 负值的右/下 padding 会使 SVG 向右/下扩展,实现右/下裁剪
124+ width_expansion = abs (min (0 , left_padding )) + abs (min (0 , right_padding ))
125+ height_expansion = abs (min (0 , top_padding )) + abs (min (0 , bottom_padding ))
126+
127+ final_width = actual_svg_width + width_expansion
128+ final_height = actual_svg_height + height_expansion
129+
130+ # 确保不超出按钮边界
131+ final_width = min (final_width , button_width - left_margin - right_margin )
132+ final_height = min (final_height , button_height - top_margin - bottom_margin )
133+
134+ # 设置 SVG 控件的几何位置
135+ self .svg_widget .setGeometry (
136+ int (final_x ),
137+ int (final_y ),
138+ int (final_width ),
139+ int (final_height )
140+ )
141+
142+ def setMargin (self , margin ):
143+ """设置 SVG 图标的外边距
144+
145+ Args:
146+ margin: (左, 上, 右, 下) 边距值的元组
147+ """
148+ self .margin = margin
149+ self .centerSvg () # 更新 SVG 位置
150+
151+ def setPadding (self , padding ):
152+ """设置 SVG 图标的内边距
153+
154+ Args:
155+ padding: (左, 上, 右, 下) 内边距值的元组
156+ """
157+ self .padding = padding
158+ self .centerSvg () # 更新 SVG 位置
159+
160+ def resizeEvent (self , event ):
161+ """重写调整大小事件,确保 SVG 图标始终居中"""
162+ super ().resizeEvent (event )
163+ self .centerSvg ()
164+
165+ def setSvgColor (self , color ):
166+ """设置 SVG 的颜色(仅当 SVG 支持颜色修改时有效)
167+
168+ Args:
169+ color: QColor 对象或颜色名称字符串
170+ """
171+ if isinstance (color , str ):
172+ color = QColor (color )
173+
174+ # 通过样式表设置 SVG 颜色
175+ self .svg_widget .setStyleSheet (f"background-color: transparent; color: { color .name ()} ;" )
176+
177+ def setSvgPath (self , svg_path ):
178+ """更新 SVG 图标
179+
180+ Args:
181+ svg_path: SVG 文件路径
182+ """
183+ # 加载 SVG 文件
184+ self .svg_widget .load (svg_path )
185+
186+ def setSvgSize (self , size ):
187+ """设置 SVG 图标的大小
188+
189+ Args:
190+ size: (width, height) 元组
191+ """
192+ self .svg_size = size
193+ self .centerSvg () # 更新 SVG 位置
194+
195+ def setBorderColor (self , color ):
196+ """设置边框颜色
197+
198+ Args:
199+ color: QColor 对象或颜色名称字符串
200+ """
201+ if isinstance (color , str ):
202+ color = QColor (color )
203+ self .border_color = color
204+ self .update () # 触发重绘
205+
206+ def setBorderWidth (self , width ):
207+ """设置边框宽度
208+
209+ Args:
210+ width: 边框宽度(像素)
211+ """
212+ self .border_width = width
213+ self .update () # 触发重绘
214+
215+ def enterEvent (self , event ):
216+ """鼠标进入事件"""
217+ self .is_hovered = True
218+ self .update () # 触发重绘
219+ super ().enterEvent (event )
220+
221+ def leaveEvent (self , event ):
222+ """鼠标离开事件"""
223+ self .is_hovered = False
224+ self .update () # 触发重绘
225+ super ().leaveEvent (event )
226+
227+ def mousePressEvent (self , event ):
228+ """鼠标按下事件"""
229+ if event .button () == Qt .LeftButton :
230+ self .is_pressed = True
231+ self .update () # 触发重绘
232+ super ().mousePressEvent (event )
233+
234+ def mouseReleaseEvent (self , event ):
235+ """鼠标释放事件"""
236+ if event .button () == Qt .LeftButton :
237+ self .is_pressed = False
238+ self .update () # 触发重绘
239+ super ().mouseReleaseEvent (event )
240+
241+ def paintEvent (self , event ):
242+ """重写绘制事件,添加圆形边框和背景"""
243+ # 不调用父类的绘制方法,完全自定义绘制
244+
245+ # 创建画笔
246+ painter = QPainter (self )
247+ painter .setRenderHint (QPainter .Antialiasing ) # 抗锯齿
248+
249+ # 根据状态设置背景颜色
250+ if self .is_pressed :
251+ # 按下状态 - 50% 透明度
252+ bg_color = QColor (255 , 255 , 255 , 127 )
253+ border_color = QColor (self .border_color )
254+ border_color .setAlpha (127 )
255+ elif self .is_hovered :
256+ # 悬停状态 - 50% 透明度
257+ bg_color = QColor (255 , 255 , 255 , 127 )
258+ border_color = QColor (self .border_color )
259+ border_color .setAlpha (127 )
260+ else :
261+ # 正常状态 - 完全透明
262+ bg_color = QColor (255 , 255 , 255 , 0 )
263+ border_color = QColor (self .border_color )
264+
265+ # 绘制背景
266+ painter .setBrush (QBrush (bg_color ))
267+
268+ # 设置画笔属性
269+ pen = QPen (border_color )
270+ pen .setWidth (self .border_width )
271+ painter .setPen (pen )
272+
273+ # 绘制圆形边框和背景
274+ painter .drawEllipse (self .rect ().adjusted (
275+ self .border_width // 2 ,
276+ self .border_width // 2 ,
277+ - self .border_width // 2 ,
278+ - self .border_width // 2
279+ ))
280+
281+ def setHoverColor (self , color ):
282+ """设置鼠标悬停时的背景颜色
283+
284+ Args:
285+ color: 颜色值,可以是 rgba 格式
286+ """
287+ if isinstance (color , str ):
288+ color = QColor (color )
289+ self .hover_color = color
290+
291+ def setAnimated (self , animated = True ):
292+ """设置是否启用动画效果
293+
294+ Args:
295+ animated: 是否启用动画
296+ """
297+ if animated :
298+ # 创建缩放动画
299+ self .animation = QPropertyAnimation (self , b"geometry" )
300+ self .animation .setDuration (100 )
301+ self .animation .setEasingCurve (QEasingCurve .OutCubic )
302+
303+ # 连接鼠标事件
304+ self ._original_enter_event = self .enterEvent
305+ self ._original_leave_event = self .leaveEvent
306+
307+ self .enterEvent = self ._animated_enter_event
308+ self .leaveEvent = self ._animated_leave_event
309+ else :
310+ # 移除动画效果
311+ if hasattr (self , '_original_enter_event' ) and hasattr (self , '_original_leave_event' ):
312+ self .enterEvent = self ._original_enter_event
313+ self .leaveEvent = self ._original_leave_event
314+
315+ def _animated_enter_event (self , event ):
316+ """鼠标进入事件(带动画)"""
317+ # 调用原始的 enterEvent 来处理状态
318+ self ._original_enter_event (event )
319+
320+ # 保存原始几何信息
321+ rect = self .geometry ()
322+
323+ # 计算放大后的几何信息(放大 5%)
324+ center_x = rect .x () + rect .width () / 2
325+ center_y = rect .y () + rect .height () / 2
326+ new_width = rect .width () * 1.05
327+ new_height = rect .height () * 1.05
328+ new_x = center_x - new_width / 2
329+ new_y = center_y - new_height / 2
330+
331+ # 设置动画
332+ self .animation .setStartValue (rect )
333+ self .animation .setEndValue (QRect (new_x , new_y , new_width , new_height ))
334+ self .animation .start ()
335+
336+ def _animated_leave_event (self , event ):
337+ """鼠标离开事件(带动画)"""
338+ # 调用原始的 leaveEvent 来处理状态
339+ self ._original_leave_event (event )
340+
341+ # 获取当前几何信息
342+ current_rect = self .geometry ()
343+
344+ # 计算原始几何信息
345+ center_x = current_rect .x () + current_rect .width () / 2
346+ center_y = current_rect .y () + current_rect .height () / 2
347+ orig_width = current_rect .width () / 1.05
348+ orig_height = current_rect .height () / 1.05
349+ orig_x = center_x - orig_width / 2
350+ orig_y = center_y - orig_height / 2
351+
352+ # 设置动画
353+ self .animation .setStartValue (current_rect )
354+ self .animation .setEndValue (QRect (orig_x , orig_y , orig_width , orig_height ))
355+ self .animation .start ()
0 commit comments