【问题标题】:How to connect two QGraphicsItem by drawing line between them (using mouse)如何通过在它们之间画线来连接两个 QGraphicsItem(使用鼠标)
【发布时间】:2021-01-23 11:31:18
【问题描述】:

我在场景中有一些自定义项目。我想允许用户使用鼠标连接这两个项目。我检查了this 问题中的答案,但没有规定让用户连接这两点。 (另外,请注意项目必须是可移动的)

这是我想要的演示:

我想要两个椭圆之间的连接如上图

我能知道这是怎么做到的吗?

【问题讨论】:

    标签: python pyqt pyqt5


    【解决方案1】:

    虽然proposed by JacksonPro 的解决方案很好,但我想提供一个稍微不同的概念,增加一些好处:

    • 改进的对象结构和控制;
    • 更可靠的碰撞检测;
    • 绘画通过使其更符合对象而略微简化;
    • 更好的可读性(主要是通过使用更少的变量和函数);
    • 更清晰的连接创建(线“捕捉”到控制点);
    • 可以在两侧都有控制点(也可以防止在同一侧建立连接)并在已经存在的情况下删除连接(通过再次“连接”相同的点);
    • 多个控制点之间的连接;
    • 不再是 ;-)

    我们的想法是拥有 实际 QGraphicsItem 对象 (QGraphicsEllipseItem) 和 CustomItem 的 子项 的控制点。
    这不仅简化了绘制,还改进了对象碰撞检测和管理:不需要复杂的函数来绘制新线,并且创建一个围绕其pos 绘制的椭圆围绕确保我们已经通过获取他们的scenePos() 知道了该行的目标;这也使得检测鼠标光标是否真的在控制点内变得更加容易。

    请注意,为了简单起见,我将一些属性设置为类成员。如果您想为更高级或自定义控件创建项目的子类,则应将这些参数创建为实例属性;在这种情况下,您可能更喜欢从 QGraphicsRectItem 继承:即使您仍然需要覆盖绘画以绘制圆角矩形,它也会更容易设置其属性(笔、画笔和矩形)甚至更改它们在运行时,因此您只需要在 paint() 中访问这些属性,同时还确保在 Qt 需要时正确调用更新。

    from PyQt5 import QtCore, QtGui, QtWidgets
    
    class Connection(QtWidgets.QGraphicsLineItem):
        def __init__(self, start, p2):
            super().__init__()
            self.start = start
            self.end = None
            self._line = QtCore.QLineF(start.scenePos(), p2)
            self.setLine(self._line)
    
        def controlPoints(self):
            return self.start, self.end
    
        def setP2(self, p2):
            self._line.setP2(p2)
            self.setLine(self._line)
    
        def setStart(self, start):
            self.start = start
            self.updateLine()
    
        def setEnd(self, end):
            self.end = end
            self.updateLine(end)
    
        def updateLine(self, source):
            if source == self.start:
                self._line.setP1(source.scenePos())
            else:
                self._line.setP2(source.scenePos())
            self.setLine(self._line)
    
    
    class ControlPoint(QtWidgets.QGraphicsEllipseItem):
        def __init__(self, parent, onLeft):
            super().__init__(-5, -5, 10, 10, parent)
            self.onLeft = onLeft
            self.lines = []
            # this flag **must** be set after creating self.lines!
            self.setFlags(self.ItemSendsScenePositionChanges)
    
        def addLine(self, lineItem):
            for existing in self.lines:
                if existing.controlPoints() == lineItem.controlPoints():
                    # another line with the same control points already exists
                    return False
            self.lines.append(lineItem)
            return True
    
        def removeLine(self, lineItem):
            for existing in self.lines:
                if existing.controlPoints() == lineItem.controlPoints():
                    self.scene().removeItem(existing)
                    self.lines.remove(existing)
                    return True
            return False
    
        def itemChange(self, change, value):
            for line in self.lines:
                line.updateLine(self)
            return super().itemChange(change, value)
    
    
    class CustomItem(QtWidgets.QGraphicsItem):
        pen = QtGui.QPen(QtCore.Qt.red, 2)
        brush = QtGui.QBrush(QtGui.QColor(31, 176, 224))
        controlBrush = QtGui.QBrush(QtGui.QColor(214, 13, 36))
        rect = QtCore.QRectF(0, 0, 100, 100)
    
        def __init__(self, left=False, right=False, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            self.setFlags(self.ItemIsMovable)
    
            self.controls = []
    
            for onLeft, create in enumerate((right, left)):
                if create:
                    control = ControlPoint(self, onLeft)
                    self.controls.append(control)
                    control.setPen(self.pen)
                    control.setBrush(self.controlBrush)
                    if onLeft:
                        control.setX(100)
                    control.setY(35)
    
        def boundingRect(self):
            adjust = self.pen.width() / 2
            return self.rect.adjusted(-adjust, -adjust, adjust, adjust)
    
        def paint(self, painter, option, widget=None):
            painter.save()
            painter.setPen(self.pen)
            painter.setBrush(self.brush)
            painter.drawRoundedRect(self.rect, 4, 4)
            painter.restore()
    
    
    class Scene(QtWidgets.QGraphicsScene):
        startItem = newConnection = None
        def controlPointAt(self, pos):
            mask = QtGui.QPainterPath()
            mask.setFillRule(QtCore.Qt.WindingFill)
            for item in self.items(pos):
                if mask.contains(pos):
                    # ignore objects hidden by others
                    return
                if isinstance(item, ControlPoint):
                    return item
                if not isinstance(item, Connection):
                    mask.addPath(item.shape().translated(item.scenePos()))
    
        def mousePressEvent(self, event):
            if event.button() == QtCore.Qt.LeftButton:
                item = self.controlPointAt(event.scenePos())
                if item:
                    self.startItem = item
                    self.newConnection = Connection(item, event.scenePos())
                    self.addItem(self.newConnection)
                    return
            super().mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
            if self.newConnection:
                item = self.controlPointAt(event.scenePos())
                if (item and item != self.startItem and
                    self.startItem.onLeft != item.onLeft):
                        p2 = item.scenePos()
                else:
                    p2 = event.scenePos()
                self.newConnection.setP2(p2)
                return
            super().mouseMoveEvent(event)
    
        def mouseReleaseEvent(self, event):
            if self.newConnection:
                item = self.controlPointAt(event.scenePos())
                if item and item != self.startItem:
                    self.newConnection.setEnd(item)
                    if self.startItem.addLine(self.newConnection):
                        item.addLine(self.newConnection)
                    else:
                        # delete the connection if it exists; remove the following
                        # line if this feature is not required
                        self.startItem.removeLine(self.newConnection)
                        self.removeItem(self.newConnection)
                else:
                    self.removeItem(self.newConnection)
            self.startItem = self.newConnection = None
            super().mouseReleaseEvent(event)
    
    
    def main():
        import sys
        app = QtWidgets.QApplication(sys.argv)
        scene = Scene()
    
        scene.addItem(CustomItem(left=True))
        scene.addItem(CustomItem(left=True))
    
        scene.addItem(CustomItem(right=True))
        scene.addItem(CustomItem(right=True))
    
        view = QtWidgets.QGraphicsView(scene)
        view.setRenderHints(QtGui.QPainter.Antialiasing)
    
        view.show()
    
        sys.exit(app.exec_())
    
    
    if __name__ == '__main__':
        main()
    

    一个小建议:我看到你有总是在paint方法中创建对象的习惯,即使这些值通常是“硬编码”的; Graphics View 框架最重要的方面之一是它的性能,这显然会被 python 部分降低,所以如果你有在运行时保持不变的属性(矩形、钢笔、画笔),通常最好让它们更“静态” ",至少作为实例属性,为了尽可能的简化绘画。

    【讨论】:

    • 我不明白这是如何工作的 -> def controlPointAt(self, pos): 你能解释一下吗?我还启用了橡皮筋选择,我想在用户画线时禁用它。目前我在if item: 下设置self.views()[0].setDragMode(QtWidgets.QGraphicsView.NoDrag) 并在mousePressEvent 中添加else:self.views()[0].setDragMode(QtWidgets.QGraphicsView.RubberBandDrag)。我想知道这是最好的方法还是有其他方法?
    • @JacksonPro controlPointAt 是必需的,因为itemAt() 总是返回给定坐标处的top item(基于shape() 实现),这是一个问题它可能会返回一个现有的“连接”(在我的例子中是 QGraphicsLineItem):如果一个“连接”(通常堆叠在 上方 共享zValues 的所有其他元素)与控制点发生冲突,您除非其 CustomItem 被移动(或连接到该连接的任何项目被移走),否则将无法使用该点。该函数确保始终只返回最顶部的可见控制点(如果有的话)。
    • 好的,更具体地说,我需要知道这是什么-> mask = QtGui.QPainterPath() mask.setFillRule(QtCore.Qt.WindingFill) 我确实检查了我仍然没有得到的文档。还请就橡皮筋的选择提出建议。
    • 关于橡皮筋的选择,我能想到的唯一安全的实现(基于这个例子,但也可以进一步扩展),就是将 QGraphicsView 子类化,实现它的viewportEvent 并返回@ 987654338@ 用于MouseButtonPress 事件,只要鼠标下的项目 控制点(使用上面的controlPointAt())和进一步的MouseMove 事件,否则总是返回基本实现(return super().viewportEvent(event)) .
    • @JacksonPro setFillRule 可能没有必要,因为我相信contains()的碰撞检测没有考虑到这一点,但比抱歉:在某些情况下,根据构建时使用的顺序和坐标,以不同的方式考虑复杂路径。无论如何,我建议阅读its documentation,因为它对其他情况也很有用。
    【解决方案2】:

    为此,您可能必须通过继承 QGraphicsScene 并覆盖鼠标事件来实现自己的场景类。

    这是你可以改进的代码:

    import sys
    from PyQt5 import QtWidgets, QtCore, QtGui
    
    
    class CustomItem(QtWidgets.QGraphicsItem):
        def __init__(self, pointONLeft=False, *args, **kwargs):
            super().__init__(*args, **kwargs)
    
            self.ellipseOnLeft = pointONLeft
            self.point = None
            self.endPoint =None
    
            self.isStart = None
    
            self.line = None
    
            self.setAcceptHoverEvents(True)
            self.setFlag(self.ItemIsMovable)
            self.setFlag(self.ItemSendsGeometryChanges)
    
        def addLine(self, line, ispoint):
            if not self.line:
                self.line = line
                self.isStart = ispoint
    
        def itemChange(self, change, value):
    
            if change == self.ItemPositionChange and self.scene():
                self.moveLineToCenter(value)
    
            return super(CustomItem, self).itemChange(change, value)
    
        def moveLineToCenter(self, newPos): # moves line to center of the ellipse
    
            if self.line:
    
                if self.ellipseOnLeft:
                    xOffset = QtCore.QRectF(-5, 30, 10, 10).x() + 5
                    yOffset = QtCore.QRectF(-5, 30, 10, 10).y() + 5
    
                else:
                    xOffset = QtCore.QRectF(95, 30, 10, 10).x() + 5
                    yOffset = QtCore.QRectF(95, 30, 10, 10).y() + 5
    
                newCenterPos = QtCore.QPointF(newPos.x() + xOffset, newPos.y() + yOffset)
    
                p1 = newCenterPos if self.isStart else self.line.line().p1()
                p2 =  self.line.line().p2() if self.isStart else newCenterPos
    
                self.line.setLine(QtCore.QLineF(p1, p2))
    
        def containsPoint(self, pos):  # checks whether the mouse is inside the ellipse
            x = self.mapToScene(QtCore.QRectF(-5, 30, 10, 10).adjusted(-0.5, 0.5, 0.5, 0.5)).containsPoint(pos, QtCore.Qt.OddEvenFill) or \
                self.mapToScene(QtCore.QRectF(95, 30, 10, 10).adjusted(0.5, 0.5, 0.5, 0.5)).containsPoint(pos,
                                                                                                          QtCore.Qt.OddEvenFill)
    
            return x
    
        def boundingRect(self):
            return QtCore.QRectF(-5, 0, 110, 110)
    
        def paint(self, painter, option, widget):
    
            pen = QtGui.QPen(QtCore.Qt.red)
            pen.setWidth(2)
    
            painter.setPen(pen)
    
            painter.setBrush(QtGui.QBrush(QtGui.QColor(31, 176, 224)))
            painter.drawRoundedRect(QtCore.QRectF(0, 0, 100, 100), 4, 4)
    
            painter.setBrush(QtGui.QBrush(QtGui.QColor(214, 13, 36)))
    
            if self.ellipseOnLeft: # draws ellipse on left
                painter.drawEllipse(QtCore.QRectF(-5, 30, 10, 10))
    
            else: # draws ellipse on right
                painter.drawEllipse(QtCore.QRectF(95, 30, 10, 10))
    
    
    # ------------------------Scene Class ----------------------------------- #
    class Scene(QtWidgets.QGraphicsScene):
        def __init__(self):
            super(Scene, self).__init__()
            self.startPoint = None
            self.endPoint = None
    
            self.line = None
            self.graphics_line = None
    
            self.item1 = None
            self.item2 = None
    
        def mousePressEvent(self, event):
            self.line = None
            self.graphics_line = None
    
            self.item1 = None
            self.item2 = None
    
            self.startPoint = None
            self.endPoint = None
    
            if self.itemAt(event.scenePos(), QtGui.QTransform()) and isinstance(self.itemAt(event.scenePos(),
                                                                                QtGui.QTransform()), CustomItem):
    
                self.item1 = self.itemAt(event.scenePos(), QtGui.QTransform())
                self.checkPoint1(event.scenePos())
    
                if self.startPoint:
                    self.line = QtCore.QLineF(self.startPoint, self.endPoint)
                    self.graphics_line = self.addLine(self.line)
    
                    self.update_path()
    
            super(Scene, self).mousePressEvent(event)
    
        def mouseMoveEvent(self, event):
    
            if event.buttons() & QtCore.Qt.LeftButton and self.startPoint:
                self.endPoint = event.scenePos()
                self.update_path()
    
            super(Scene, self).mouseMoveEvent(event)
    
        def filterCollidingItems(self, items):  #  filters out all the colliding items and returns only instances of CustomItem
            return [x for x in items if isinstance(x, CustomItem) and x != self.item1]
    
        def mouseReleaseEvent(self, event):
    
            if self.graphics_line:
    
                self.checkPoint2(event.scenePos())
                self.update_path()
    
                if self.item2 and not self.item1.line and not self.item2.line:
                    self.item1.addLine(self.graphics_line, True)
                    self.item2.addLine(self.graphics_line, False)
    
                else:
                    if self.graphics_line:
                        self.removeItem(self.graphics_line)
    
            super(Scene, self).mouseReleaseEvent(event)
    
        def checkPoint1(self, pos):
    
            if self.item1.containsPoint(pos):
    
                self.item1.setFlag(self.item1.ItemIsMovable, False)
                self.startPoint = self.endPoint = pos
    
            else:
                self.item1.setFlag(self.item1.ItemIsMovable, True)
    
        def checkPoint2(self, pos):
    
            item_lst = self.filterCollidingItems(self.graphics_line.collidingItems())
            contains = False
    
            if not item_lst:  # checks if there are any items in the list
                return
    
            for self.item2 in item_lst:
                if self.item2.containsPoint(pos):
                    contains = True
                    self.endPoint = pos
                    break
       
            if not contains:
                self.item2 = None
    
        def update_path(self):
            if self.startPoint and self.endPoint:
                self.line.setP2(self.endPoint)
                self.graphics_line.setLine(self.line)
    
    
    def main():
        app = QtWidgets.QApplication(sys.argv)
        scene = Scene()
    
        item1 = CustomItem(True)
        scene.addItem(item1)
    
        item2 = CustomItem()
        scene.addItem(item2)
    
        view = QtWidgets.QGraphicsView(scene)
        view.setViewportUpdateMode(view.FullViewportUpdate)
        view.setMouseTracking(True)
    
        view.show()
    
        sys.exit(app.exec_())
    
    
    if __name__ == '__main__':
        main()
    

    上面代码的解释:

    我通过继承 QGraphicsItem 来制作自己的自定义项目。 pointONLeft=False 是检查要绘制椭圆的哪一侧。如果pointONLeft=True,那么您在问题图像中看到的红色圆圈将绘制在左侧。

    • addLineitemChangemoveLineToCenter 方法取自 here。我建议您在继续之前完成该答案。

    • CustomItem 内部的containsPoint 方法检查鼠标是否在圆圈内。此方法将从自定义Scene 访问,如果鼠标在圆圈内,它将使用CustomiItem.setFlag(CustomItem.ItemIsMovable, False) 禁用移动。

    • 为了画线,我使用了 PyQt 提供的QLineF。如果您想知道如何通过拖动来绘制直线,我建议您参考this。而对于qpainterpath 的解释也可以在这里应用。

    • collidingItems()QGraphicsItem 提供的方法。它返回所有发生碰撞的项目,包括线本身。因此,我创建了 filterCollidingItems 以仅过滤出属于 CustomItem 实例的项目。

    (另外,请注意collidingItems() 以相反的插入顺序返回碰撞项目,即如果首先插入 CustomItem1,然后插入 CustomItem,那么如果行碰撞,则将首先返回第二个项目。所以如果两个项目彼此在并且线发生碰撞然后最后插入的项目将变为item2您可以通过更改z value来更改它)

    读者可以在 cmets 中添加建议或查询。如果您有更好的答案,请随时写。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-13
      • 1970-01-01
      • 1970-01-01
      • 2013-06-01
      相关资源
      最近更新 更多