如何在 PyQt5 中移动带端点的线

How to move line with endpoints in PyQt5

我有一个图形场景和 2 类 条线和端点,但是当我移动线的端点时出现问题。当我移动线时,端点会移动,但是当移动端点时,它们就会离开线,即使我将其设为父线。我希望它双向工作,所以当移动端点时,线会移动到它,因此它可以改变长度和斜率。

class Lineitem(QGraphicsLineItem):
    def __init__(self):
        super().__init__()
        self.setLine(0, 0, 500, 0)
        self.setPen(QPen(QColor(0, 0, 0),  5  ))
        
        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)

        self.endpoints = []
        for i in range(2):
            endpoint = Endpoint(self)
            endpoint.setParentItem(self)
            self.endpoints.append(endpoint)

        self.endpoints[0].setPos(-5, -5)
        self.endpoints[1].setPos(495, -5)


class Endpoint(QGraphicsEllipseItem):
    def __init__(self, parent):
        super().__init__(parent)
        self.setRect(0, 0, 10, 10 )
        self.setBrush(QColor(0, 0, 255))

        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)

这是主窗口:

class MainWindow(QMainWindow):

    def __init__(self):
        super().__init__()
        self.gscene = QGraphicsScene(0, 0, 1000, 1000)
        
        self.line = Lineitem()
        self.line.setPos(300, 300)

        self.gview = QGraphicsView(self.gscene)
        self.gscene.addItem(self.line)

        self.setCentralWidget(self.gview)

这是预期的行为:child 项目与其 parent 项目一起移动,而不是相反。如果你想根据端点调整线,你需要编写一个函数来做到这一点。

class LineItem(QGraphicsLineItem):
    # ...
    def updateLine(self):
        p1, p2 = self.endpoints
        self.setLine(QLineF(p1.pos(), p2.pos()))

现在 how/where 有几个不同的选项来调用此函数(它们都实现相同的结果,但您可能更喜欢一个或发现一个更适合您的项目)。

在 CHILD 子类中

选项 1) 重新实现 mouseMoveEvent

class Endpoint(QGraphicsEllipseItem):

    def __init__(self, *args, **kwargs):
        super().__init__(-5, -5, 10, 10, *args, **kwargs)
        self.setBrush(QColor(0, 0, 255))
        self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)

    def mouseMoveEvent(self, event):
        super().mouseMoveEvent(event)
        self.parentItem().updateLine()

另请注意,我偏移了 rect,因此 pos() 实际上位于项目的中心。

选项 2) 设置标志 ItemSendsScenePositionChanges,重新实现 itemChange 以捕获 ItemScenePositionHasChanged

class Endpoint(QGraphicsEllipseItem):

    def __init__(self, *args, **kwargs):
        super().__init__(-5, -5, 10, 10, *args, **kwargs)
        self.setBrush(QColor(0, 0, 255))
        self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
        self.setFlag(self.ItemSendsScenePositionChanges)

    def itemChange(self, change, value)
        if change == self.ItemScenePositionHasChanged:
            self.parentItem().updateLine()
        return super().itemChange(change, value)

在 PARENT 子类中

选项 3) 调用 setFiltersChildEvents(True) 并重新实现 sceneEventFilter 以捕获 QEvent.GraphicsSceneMouseMove

class LineItem(QGraphicsLineItem):

    def __init__(self):
        super().__init__()
        self.setLine(0, 0, 500, 0)
        self.setPen(QPen(QColor(0, 0, 0),  5  ))
        self.setFlags(self.ItemIsMovable | self.ItemIsSelectable)
        self.endpoints = [Endpoint(self) for i in range(2)]
        self.endpoints[1].setPos(500, 0)
        self.setFiltersChildEvents(True)
        
    def sceneEventFilter(self, obj, event):
        if event.type() == QEvent.GraphicsSceneMouseMove:
            obj.mouseMoveEvent(event)
            self.updateLine()
            return True # we handled the event, prevent further processing
        return super().sceneEventFilter(obj, event)
        
    def updateLine(self):
        p1, p2 = self.endpoints
        self.setLine(QLineF(p1.pos(), p2.pos()))

请注意 setFiltersChildEvents 将从所有 child 项中过滤事件,您可能需要检查 if obj in self.endpoints。或者在每个 child 上调用 installSceneEventFilter(parent),它只能在 parent 添加到场景后使用。所以会在场景中添加线条的范围内完成,如:

line = LineItem()
scene.addItem(line)
for x in line.endpoints:
    x.installSceneEventFilter(line)

了解响应鼠标移动时调用这些方法的顺序可能也很有用:

sceneEventFilter -> mouseMoveEvent -> itemChange

  1. LineItem.sceneEventFilter 在事件被分派到事件处理程序之前拦截事件。如果它 returns False 它将通过事件系统进行。

  2. Endpoint.mouseMoveEvent mouseMoveEvent 处理程序现在接收到事件。默认实现或调用 super 将移动项目并发送启用的通知:

  3. Endpoint.itemChange 此处通知项目 ItemScenePositionHasChanged,(一个 read-only 通知)

线的 children 端点这一事实并不意味着什么:parent 并不 神奇地 知道你想要它根据其 children.
的位置而改变 在您的情况下,您只添加了两个端点,但没有什么可以阻止您添加其他 child 项:parent 怎么知道只要 children 中的任何一个被移动就该做什么?

如果您想根据端点更新线路,您需要自行实施,方法是:

  • 拦截位置变化,通过设置 ItemSendsGeometryChanges 标志并捕获 ItemPositionHasChanged 覆盖 itemChange();
  • 通知 parent 职位变动;

考虑到像 QGraphicsLineItem 和 QGraphicsEllipseItem 这样的基本 QGraphicsItem 不是 从 QObject 继承,你不能使用信号,但如果你确定这些项目将 always 是你的 Lineitem children class,然后你可以安全地调用一个 parent item 函数来重建新行:

class Lineitem(QGraphicsLineItem):
    def __init__(self):
        super().__init__()
        self.setLine(0, 0, 500, 0)
        self.setPen(QPen(QColor(0, 0, 0),  5  ))
        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)
        self.endpoints = []
        
        for i in range(2):
            endpoint = Endpoint(self)
            endpoint.setParentItem(self)
            self.endpoints.append(endpoint)
        self.endpoints[1].setPos(500, 0)

    def updateLine(self):
        self.setLine(QLineF(self.endpoints[0].pos(), self.endpoints[1].pos()))


class Endpoint(QGraphicsEllipseItem):
    def __init__(self, parent):
        super().__init__(parent)
        self.setRect(-5, -5, 10, 10)
        self.setBrush(QColor(0, 0, 255))
        self.setFlag(self.ItemIsMovable)
        self.setFlag(self.ItemIsSelectable)
        self.setFlag(self.ItemSendsGeometryChanges)

    def itemChange(self, change, value):
        if change == self.ItemPositionHasChanged:
            self.parentItem().updateLine()
        return super().itemChange(change, value)

请注意,我更改了椭圆的矩形,以使它们始终以项目坐标系的原点 (0, 0) 为中心。
这可确保您无论何时移动它们,它们始终位于正确的坐标处,而不会试图将它们“固定”到一半大小。