PyQt 实现弧形旋转菜单源码分享
一、源码分享
1、源码分享
import sys
import math
from PyQt6.QtCore import Qt, QPointF, QRectF, QTimer
from PyQt6.QtGui import QPainter, QBrush, QColor, QTransform
from PyQt6.QtSvg import QSvgRenderer
from PyQt6.QtWidgets import QApplication, QWidget, QLabel
# 图标路径
CARD_DATA = [
{"color": "#add8e6", "icon": "image/11.svg"},
{"color": "#90ee90", "icon": "image/22.svg"},
{"color": "#f08080", "icon": "image/33.svg"},
{"color": "#ffff00", "icon": "image/44.svg"},
{"color": "#ff00ff", "icon": "image/55.svg"},
]
CARD_COUNT = len(CARD_DATA)
CARD_SIZE = 60
CARD_RADIUS = 30
ARC_RADIUS = 100
PATH_CENTER_Y = 100
class ArcCardWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setFixedSize(400, 400)
self.current_index = 0
self.card_items = []
# 拖拽基础参数
self.t_offset = 0.0
self.mouse_last_pos = QPointF()
self.is_dragging = False
self.drag_sensitivity = 0.002
# 惯性滑行
self.velocity = 0.0
self.friction = 0.92
self.inertia_timer = QTimer(self)
self.inertia_timer.setInterval(16)
self.inertia_timer.timeout.connect(self.do_inertia_slide)
# 点击归位动画
self.anim_timer = QTimer(self)
self.anim_timer.setInterval(16)
self.anim_timer.timeout.connect(self.animate_to_center)
self.target_t_offset = 0.0
self.anim_speed = 0.12 # 越大归位越快
# SVG缓存
self.svg_cache = {}
for item in CARD_DATA:
path = item["icon"]
self.svg_cache[path] = QSvgRenderer(path)
self.calc_card_params()
def calc_card_params(self):
self.card_items.clear()
arc_cx = 80
arc_cy = PATH_CENTER_Y + ARC_RADIUS
total_steps = CARD_COUNT
for idx in range(total_steps):
base_t = idx / (total_steps - 1)
t = base_t + self.t_offset
t = max(-0.6, min(1.6, t))
angle_deg = (t - 0.5) * 180
rad = math.radians(angle_deg)
px = arc_cx + ARC_RADIUS * math.cos(rad)
py = arc_cy + ARC_RADIUS * math.sin(rad)
pos = QPointF(px, py)
dist_center = abs(t - 0.5)
scale = 1.0 - dist_center * 0.5
scale = max(0.4, scale)
rot = angle_deg
z_order = t
self.card_items.append({
"index": idx,
"pos": pos,
"scale": scale,
"rot": rot,
"z": z_order
})
self.card_items.sort(key=lambda x: x["z"])
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
for item in self.card_items:
idx = item["index"]
data = CARD_DATA[idx]
pos = item["pos"]
scale = item["scale"]
rot = item["rot"]
color = QColor(data["color"])
icon_path = data["icon"]
painter.save()
painter.translate(pos.x() + CARD_RADIUS, pos.y() + CARD_RADIUS)
painter.rotate(rot)
painter.scale(scale, scale)
painter.setBrush(QBrush(color))
painter.setPen(Qt.PenStyle.NoPen)
ellipse_rect = QRectF(-CARD_RADIUS, -CARD_RADIUS, CARD_SIZE, CARD_SIZE)
painter.drawEllipse(ellipse_rect)
svg_render = self.svg_cache.get(icon_path)
if svg_render and svg_render.isValid():
icon_rect = QRectF(-25, -25, 50, 50)
svg_render.render(painter, icon_rect)
else:
painter.setBrush(QColor("#666666"))
painter.drawRect(QRectF(-20, -20, 40, 40))
painter.restore()
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self.inertia_timer.stop()
self.anim_timer.stop()
self.velocity = 0.0
self.mouse_last_pos = event.position()
hit_card = None
click_pt = event.position()
for item in reversed(self.card_items):
pos = item["pos"]
scale = item["scale"]
rot = item["rot"]
tf = QTransform()
tf.translate(pos.x() + CARD_RADIUS, pos.y() + CARD_RADIUS)
tf.rotate(rot)
tf.scale(scale, scale)
inv_tf, ok = tf.inverted()
if not ok:
continue
local_pt = inv_tf.map(click_pt)
dist = math.hypot(local_pt.x(), local_pt.y())
if dist <= CARD_RADIUS:
hit_card = item["index"]
break
if hit_card is not None:
self.current_index = hit_card
self.window().update_text(hit_card)
# 计算目标偏移:让点击的卡片t=0.5(3点钟)
target_base_t = hit_card / (CARD_COUNT - 1)
self.target_t_offset = 0.5 - target_base_t
# 启动归位动画
self.anim_timer.start()
self.is_dragging = False
else:
self.is_dragging = True
def animate_to_center(self):
# 缓动插值趋近目标偏移
diff = self.target_t_offset - self.t_offset
if abs(diff) < 0.0008:
self.t_offset = self.target_t_offset
self.anim_timer.stop()
self.calc_card_params()
self.update()
return
self.t_offset += diff * self.anim_speed
self.t_offset = max(-0.6, min(0.6, self.t_offset))
self.calc_card_params()
self.update()
def mouseMoveEvent(self, event):
if not self.is_dragging:
return
self.anim_timer.stop()
delta_x = event.position().x() - self.mouse_last_pos.x()
move_step = delta_x * self.drag_sensitivity
self.t_offset -= move_step
self.velocity = move_step
self.t_offset = max(-0.6, min(0.6, self.t_offset))
self.mouse_last_pos = event.position()
self.calc_card_params()
self.update()
def mouseReleaseEvent(self, event):
self.is_dragging = False
if abs(self.velocity) > 1e-5:
self.inertia_timer.start()
def do_inertia_slide(self):
self.velocity *= self.friction
if abs(self.velocity) < 1e-5:
self.inertia_timer.stop()
self.velocity = 0
return
self.t_offset -= self.velocity
self.t_offset = max(-0.6, min(0.6, self.t_offset))
self.calc_card_params()
self.update()
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("PyQt6 点击图标自动滑到3点钟")
self.setFixedSize(800, 600)
self.setStyleSheet("background-color: #020414;")
self.arc_view = ArcCardWidget(self)
self.arc_view.move(0, 100)
self.text_label = QLabel("", self)
self.text_label.setStyleSheet("font-size:30px; color:white;")
self.text_label.setGeometry(400, 250, 300, 200)
def update_text(self, index):
self.text_label.setText(f"index :{index} clicked!")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = MainWindow()
win.show()
sys.exit(app.exec())
2、效果展示



3、库安装

4、完整工程下载
二、QTransform类详解
1、 QTransform类概述
QTransform是PyQt6中用于2D图形变换的核心类,它封装了一个3×3的变换矩阵,用于实现坐标系统的平移、旋转、缩放、剪切等几何变换。在Qt的绘图系统中,QTransform是QPainter进行图形绘制时的重要工具。
1.1、 主要特性
- 仿射变换:支持平移、旋转、缩放、剪切等线性变换
- 矩阵运算:基于3×3齐次坐标矩阵实现
- 组合变换:支持多个变换的级联(矩阵乘法)
- 逆变换:可以计算变换的逆矩阵
- 类型判断:可以判断变换类型(平移、旋转、缩放等)
2、 变换矩阵结构
QTransform使用3×3齐次坐标矩阵:
[ m11 m12 m13 ]
[ m21 m22 m23 ]
[ m31 m32 m33 ]
其中:
m11, m12, m21, m22:线性变换部分(旋转、缩放、剪切)m13, m23:平移分量m31, m32:透视变换分量(通常为0)m33:齐次坐标缩放因子(通常为1)
3、基本变换操作
3.1、 创建变换对象
# 创建单位矩阵(无变换)
transform1 = QTransform()
# 创建指定矩阵元素的变换
transform2 = QTransform(m11, m12, m13,
m21, m22, m23,
m31, m32, m33)
# 从现有变换复制
transform3 = QTransform(transform2)
3.2、 平移变换
# 方法1:使用translate()方法
transform = QTransform()
transform.translate(100, 50) # x方向平移100,y方向平移50
# 方法2:直接设置平移分量
transform.setMatrix(1, 0, 100, # m13 = 100
0, 1, 50, # m23 = 50
0, 0, 1)
3.3、 旋转变换
# 绕原点旋转
transform = QTransform()
transform.rotate(45) # 顺时针旋转45度
# 绕指定点旋转
transform.rotate(30, Qt.Axis.ZAxis) # 绕Z轴旋转30度
# 使用角度和轴心点
angle = 60
pivot = QPointF(100, 100)
transform = QTransform()
transform.translate(pivot.x(), pivot.y()) # 移动到轴心
transform.rotate(angle) # 旋转
transform.translate(-pivot.x(), -pivot.y()) # 移回
3.4、 缩放变换
# 均匀缩放
transform = QTransform()
transform.scale(2.0, 2.0) # 放大2倍
# 非均匀缩放
transform.scale(1.5, 0.8) # x方向1.5倍,y方向0.8倍
# 绕指定点缩放
def scaleAround(point, sx, sy):
transform = QTransform()
transform.translate(point.x(), point.y())
transform.scale(sx, sy)
transform.translate(-point.x(), -point.y())
return transform
3.5、 剪切变换
# 水平剪切
transform = QTransform()
transform.shear(0.5, 0) # 水平剪切系数0.5
# 垂直剪切
transform.shear(0, 0.3) # 垂直剪切系数0.3
# 双向剪切
transform.shear(0.2, 0.1) # 水平0.2,垂直0.1
4、 变换组合与级联
4.1、 矩阵乘法组合
# 创建平移变换
translate = QTransform()
translate.translate(100, 50)
# 创建旋转变换
rotate = QTransform()
rotate.rotate(45)
# 创建缩放变换
scale = QTransform()
scale.scale(2, 2)
# 组合变换:先缩放,再旋转,最后平移
combined = translate * rotate * scale
# 注意:矩阵乘法不满足交换律,顺序很重要!
# translate * rotate ≠ rotate * translate
4.2、 使用QPainter应用变换
def paintEvent(self, event):
painter = QPainter(self)
# 方法1:使用setTransform()替换当前变换
transform = QTransform()
transform.translate(100, 50)
transform.rotate(30)
painter.setTransform(transform)
# 绘制图形(应用上述变换)
painter.drawRect(0, 0, 50, 50)
# 方法2:使用translate()、rotate()、scale()等便捷方法
painter.save() # 保存当前状态
painter.translate(200, 100)
painter.rotate(45)
painter.scale(1.5, 1.5)
painter.drawEllipse(-25, -25, 50, 50)
painter.restore() # 恢复之前的状态
5、逆变换与坐标映射
5.1、 计算逆变换
transform = QTransform()
transform.translate(100, 50)
transform.rotate(30)
# 计算逆变换
inverse_transform, ok = transform.inverted()
if ok:
# 使用逆变换将点映射回原始坐标
point = QPointF(150, 100)
original_point = inverse_transform.map(point)
5.2、 坐标映射方法
transform = QTransform()
transform.translate(50, 30)
transform.scale(2, 2)
# 映射单个点
point = QPointF(10, 20)
transformed_point = transform.map(point)
# 映射矩形(返回多边形)
rect = QRectF(0, 0, 100, 50)
polygon = transform.mapToPolygon(rect.toRect()) # 返回QPolygon
# 映射矩形(返回变换后的矩形边界)
bounding_rect = transform.mapRect(rect)
# 映射多边形
poly = QPolygonF([QPointF(0, 0), QPointF(50, 0), QPointF(25, 50)])
transformed_poly = transform.map(poly)
6、变换类型判断与属性获取
6.1、 判断变换类型
transform = QTransform()
# 判断是否为恒等变换(单位矩阵)
if transform.isIdentity():
print("这是单位矩阵")
# 判断是否只包含平移
if transform.isTranslating():
print("只包含平移变换")
# 判断是否只包含缩放
if transform.isScaling():
print("只包含缩放变换")
# 判断是否为旋转或镜像
if transform.isRotating():
print("包含旋转变换")
# 获取变换类型
transform_type = transform.type()
# 返回值可能是:
# QTransform.TransformType.TxNone # 无变换
# QTransform.TransformType.TxTranslate # 平移
# QTransform.TransformType.TxScale # 缩放
# QTransform.TransformType.TxRotate # 旋转
# QTransform.TransformType.TxShear # 剪切
# QTransform.TransformType.TxProject # 投影
6.2、 获取变换参数
transform = QTransform()
transform.translate(100, 50)
transform.rotate(45)
transform.scale(2, 1.5)
# 获取平移分量
dx = transform.dx() # m13
dy = transform.dy() # m23
# 获取缩放因子
m11 = transform.m11() # x方向缩放
m22 = transform.m22() # y方向缩放
# 获取旋转角度(弧度)
angle_rad = transform.angle() # 返回弧度
# 获取剪切参数
sh = transform.shear() # 水平剪切
sv = transform.shear(False) # 垂直剪切
# 获取行列式(判断是否可逆)
det = transform.determinant()
if abs(det) > 1e-6:
print("变换可逆")
7、 实际应用示例
7.1、 示例1:卡片旋转动画(如源码所示)
在提供的源码中,QTransform用于实现卡片的点击检测:
def mousePressEvent(self, event):
# ... 其他代码 ...
for item in reversed(self.card_items):
pos = item["pos"]
scale = item["scale"]
rot = item["rot"]
tf = QTransform()
tf.translate(pos.x() + CARD_RADIUS, pos.y() + CARD_RADIUS)
tf.rotate(rot)
tf.scale(scale, scale)
inv_tf, ok = tf.inverted()
if not ok:
continue
local_pt = inv_tf.map(click_pt)
# ... 碰撞检测 ...
关键点分析:
- 创建从世界坐标到卡片局部坐标的变换矩阵
- 使用逆变换将点击点映射到卡片局部坐标系
- 在局部坐标系中进行圆形碰撞检测
7.2 、示例2:图形编辑器中的变换工具
class ShapeItem:
def __init__(self):
self.transform = QTransform()
self.position = QPointF(0, 0)
self.rotation = 0
self.scale = 1.0
def apply_transform(self):
"""应用当前变换到绘图"""
self.transform = QTransform()
self.transform.translate(self.position.x(), self.position.y())
self.transform.rotate(self.rotation)
self.transform.scale(self.scale, self.scale)
def paint(self, painter):
painter.save()
painter.setTransform(self.transform, True) # combine=True
# 绘制形状...
painter.restore()
def contains_point(self, point):
"""检查点是否在形状内(考虑变换)"""
inv_transform, ok = self.transform.inverted()
if not ok:
return False
local_point = inv_transform.map(point)
# 在局部坐标系中检查...
return self.local_bounds.contains(local_point)
7.3、 示例3:视图缩放与平移
class ZoomableView(QWidget):
def __init__(self):
super().__init__()
self.view_transform = QTransform()
self.scale_factor = 1.0
self.pan_offset = QPointF(0, 0)
def wheelEvent(self, event):
"""鼠标滚轮缩放"""
zoom_point = event.position()
old_zoom = self.scale_factor
# 计算缩放因子
if event.angleDelta().y() > 0:
self.scale_factor *= 1.1
else:
self.scale_factor /= 1.1
# 限制缩放范围
self.scale_factor = max(0.1, min(10.0, self.scale_factor))
# 以鼠标位置为中心缩放
scale_change = self.scale_factor / old_zoom
self.pan_offset = zoom_point + (self.pan_offset - zoom_point) * scale_change
self.update_transform()
self.update()
def update_transform(self):
"""更新视图变换矩阵"""
self.view_transform = QTransform()
self.view_transform.translate(self.pan_offset.x(), self.pan_offset.y())
self.view_transform.scale(self.scale_factor, self.scale_factor)
def paintEvent(self, event):
painter = QPainter(self)
painter.setTransform(self.view_transform)
# 绘制场景内容...
8、性能优化建议
8.1、 变换缓存
# 避免在paintEvent中重复计算变换
class OptimizedWidget(QWidget):
def __init__(self):
super().__init__()
self.cached_transform = None
self.transform_dirty = True
def update_transform(self):
if self.transform_dirty:
self.cached_transform = self.calculate_transform()
self.transform_dirty = False
def paintEvent(self, event):
self.update_transform()
painter = QPainter(self)
painter.setTransform(self.cached_transform)
# ... 绘制代码 ...
8.2 矩阵预计算
# 对于静态变换,预先计算组合矩阵
static_transforms = []
for i in range(100):
transform = QTransform()
transform.translate(i * 10, i * 5)
transform.rotate(i * 3)
static_transforms.append(transform)
# 绘制时直接使用预计算的变换
for transform in static_transforms:
painter.setTransform(transform)
painter.drawRect(0, 0, 20, 20)
9、 常见问题与调试技巧
9.1、 变换顺序问题
# 错误示例:顺序错误导致意外结果
transform = QTransform()
transform.translate(100, 100) # 先平移
transform.rotate(45) # 再旋转(绕原点旋转,不是绕(100,100))
# 正确示例:先旋转后平移
transform = QTransform()
transform.rotate(45) # 先旋转(绕原点)
transform.translate(100, 100) # 再平移
9.2、 逆变换失败处理
transform = QTransform()
transform.scale(0, 0) # 缩放因子为0,矩阵不可逆
inverse, ok = transform.inverted()
if not ok:
# 处理不可逆的情况
print("警告:变换矩阵不可逆,使用单位矩阵代替")
inverse = QTransform()
9.3、 浮点精度问题
# 使用容差比较变换
def transforms_almost_equal(t1, t2, tolerance=1e-6):
return (abs(t1.m11() - t2.m11()) < tolerance and
abs(t1.m12() - t2.m12()) < tolerance and
# ... 比较所有矩阵元素 ...
abs(t1.m33() - t2.m33()) < tolerance)
10、总结
QTransform是PyQt6中强大的2D图形变换工具,掌握它可以:
- 实现复杂的图形动画效果
- 创建交互式图形编辑器
- 优化图形渲染性能
- 处理坐标系统转换
在实际使用中,需要注意变换的顺序、矩阵的可逆性以及性能优化。通过合理使用QTransform,可以大大增强PyQt6应用程序的图形表现力。

692

被折叠的 条评论
为什么被折叠?



