You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PyQt5 QGraphicsView样条编辑器:控制点与样条定位对齐问题

解决PyQt5 QGraphicsView中B样条控制点与连线端点对齐问题

问题原因

PyQt5实现中控制点位置逻辑错误:

  • 创建椭圆时,用x-5,y-5作为图元自身坐标系的左上角,随后又调用setPos(x,y)设置图元在场景中的位置,导致椭圆中心点实际位置偏移了一倍的初始坐标。
  • 后续计算样条和连线时额外添加+5偏移,进一步加剧了错位。

解决方案

  1. 修正控制点位置逻辑:让椭圆中心点与图元自身坐标系原点重合,通过setPos(x,y)直接将中心点定位到场景目标坐标。
  2. 移除额外偏移计算:直接使用scenePos()获取控制点中心坐标,不再添加+5偏移。
  3. 优化更新触发逻辑:监听控制点位置变化事件,仅在必要时刷新曲线,避免无效重复计算。

修改后的完整代码

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.11 01:55:53