PyQt5 QGraphicsView样条编辑器:控制点与样条定位对齐问题
解决PyQt5 QGraphicsView中B样条控制点与连线端点对齐问题
问题原因
PyQt5实现中控制点位置逻辑错误:
- 创建椭圆时,用
x-5,y-5作为图元自身坐标系的左上角,随后又调用setPos(x,y)设置图元在场景中的位置,导致椭圆中心点实际位置偏移了一倍的初始坐标。 - 后续计算样条和连线时额外添加
+5偏移,进一步加剧了错位。
解决方案
- 修正控制点位置逻辑:让椭圆中心点与图元自身坐标系原点重合,通过
setPos(x,y)直接将中心点定位到场景目标坐标。 - 移除额外偏移计算:直接使用
scenePos()获取控制点中心坐标,不再添加+5偏移。 - 优化更新触发逻辑:监听控制点位置变化事件,仅在必要时刷新曲线,避免无效重复计算。
修改后的完整代码
import numpy as np import scipy.interpolate as si from PyQt5 import QtWidgets, QtCore, QtGui from PyQt5.QtCore import Qt, QObject from PyQt5.QtGui import QPen, QBrush class MovablePoint(QtWidgets.QGraphicsEllipseItem): positionChanged = QtCore.pyqtSignal() def __init__(self, x, y): super().__init__(-5, -5, 10, 10) self.setBrush(QBrush(Qt.red)) self.setPen(QPen(Qt.red)) self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable) self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges) self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable) self.setZValue(1) self.setPos(x, y) def itemChange(self, change, value): if change == QtWidgets.QGraphicsItem.ItemPositionChange: self.positionChanged.emit() return super().itemChange(change, value) class LineEditorApp: def __init__(self): self.root = QtWidgets.QWidget() self.root.setWindowTitle("Line Editor App") self.canvas = QtWidgets.QGraphicsView(self.root) self.canvas.setGeometry(0, 0, 500, 500) self.scene = QtWidgets.QGraphicsScene() self.canvas.setScene(self.scene) self.points = [self.create_movable_point(x, 200) for x in [120, 170, 220, 270]] self.line = None self.point_lines = [] self.update_line() self.root.show() def create_movable_point(self, x, y): point = MovablePoint(x, y) point.positionChanged.connect(self.update_line) self.scene.addItem(point) return point def update_line(self): if self.line: self.scene.removeItem(self.line) if self.point_lines: for line in self.point_lines: self.scene.removeItem(line) # 获取所有控制点的中心坐标 x_coords = [point.scenePos().x() for point in self.points] y_coords = [point.scenePos().y() for point in self.points] degree = 3 spline_points = bspline(list(zip(x_coords, y_coords)), degree=degree) line_coords = [(int(x), int(y)) for x, y in spline_points] # 创建样条曲线 line_path = QtGui.QPainterPath() line_path.moveTo(*line_coords[0]) for coord in line_coords[1:]: line_path.lineTo(*coord) self.line = self.scene.addPath(line_path, QPen(Qt.blue, 2, Qt.SolidLine)) self.line.setZValue(0) # 创建控制点之间的黑色连线 self.point_lines = [] for i in range(len(self.points) - 1): pos1 = self.points[i].scenePos() pos2 = self.points[i + 1].scenePos() point_line = self.scene.addLine(pos1.x(), pos1.y(), pos2.x(), pos2.y()) point_line.setPen(QPen(Qt.black, 1)) point_line.setZValue(0) self.point_lines.append(point_line) self.scene.update() def bspline(cv, n=100, degree=3): """ Calculate n samples on a B-spline cv : Array of control vertices n : Number of samples to return degree: Curve degree """ cv = np.asarray(cv) count = cv.shape[0] # Prevent degree from exceeding count-1, otherwise splev will crash degree = np.clip(degree, 1, count - 1) # Calculate knot vector kv = np.array([0] * degree + list(range(count - degree + 1)) + [count - degree] * degree, dtype='int') # Calculate query range u = np.linspace(0, (count - degree), n) # Calculate result return np.array(si.splev(u, (kv, cv.T, degree))).T if __name__ == '__main__': app = QtWidgets.QApplication([]) window = LineEditorApp() app.exec_()
关键修改说明
- 自定义
MovablePoint类,将椭圆设置为以自身原点为中心,确保scenePos()直接返回中心点坐标。 - 通过
itemChange方法监听位置变化,触发精准的曲线更新。 - 移除所有坐标计算中的
+5偏移,保证红色控制点中心与黑色连线端点、样条定义点完全重合。
内容的提问来源于stack exchange,提问作者Inês Teiga
相关产品推荐
相关产品推荐

