如何通过在它们之间画线来连接两个 QGraphicsItem(使用鼠标) [英] How to connect two QGraphicsItem by drawing line between them (using mouse)

查看:216
本文介绍了如何通过在它们之间画线来连接两个 QGraphicsItem(使用鼠标)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我在场景中有一些自定义项目.我想允许用户使用鼠标连接这两个项目.我在

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

我能知道如何做到这一点吗?

解决方案

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

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

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

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

from PyQt5 import QtCore, QtGui, QtWidgets类连接(QtWidgets.QGraphicsLineItem):def __init__(self, start, p2):super().__init__()self.start = 开始self.end = 无self._line = QtCore.QLineF(start.scenePos(), p2)self.setLine(self._line)定义控制点(自我):返回 self.start, self.enddef setP2(self, p2):self._line.setP2(p2)self.setLine(self._line)def setStart(self, start):self.start = 开始self.updateLine()def setEnd(self, end):self.end = 结束self.updateLine(end)def updateLine(self, source):如果源== self.start:self._line.setP1(source.scenePos())别的:self._line.setP2(source.scenePos())self.setLine(self._line)类控制点(QtWidgets.QGraphicsEllipseItem):def __init__(self, parent, onLeft):super().__init__(-5, -5, 10, 10, 父)self.onLeft = onLeftself.lines = []# 这个标志**必须**在创建self.lines之后设置!self.setFlags(self.ItemSendsScenePositionChanges)def addLine(self, lineItem):存在于 self.lines 中:如果existing.controlPoints() == lineItem.controlPoints():# 具有相同控制点的另一条线已经存在返回错误self.lines.append(lineItem)返回真def removeLine(self, lineItem):存在于 self.lines 中:如果existing.controlPoints() == lineItem.controlPoints():self.scene().removeItem(现有)self.lines.remove(现有)返回真返回错误def itemChange(self, change, value):对于 self.lines 中的行:line.updateLine(self)返回 super().itemChange(change, value)类 CustomItem(QtWidgets.QGraphicsItem):pen = QtGui.QPen(QtCore.Qt.red, 2)画笔 = 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 = []对于 onLeft,在 enumerate((right, left)) 中创建:如果创建:control = ControlPoint(self, onLeft)self.controls.append(control)control.setPen(self.pen)control.setBrush(self.controlBrush)如果在左:control.setX(100)control.setY(35)def boundingRect(self):调整 = self.pen.width()/2返回 self.rect.adjusted(-adjust, -adjust, adjust, adjust)定义油漆(自我,画家,选项,小部件=无):画家.save()Painter.setPen(self.pen)Painter.setBrush(self.brush)Painter.drawRoundedRect(self.rect, 4, 4)画家.恢复()类场景(QtWidgets.QGraphicsScene):startItem = newConnection = 无def controlPointAt(self, pos):掩码 = QtGui.QPainterPath()mask.setFillRule(QtCore.Qt.WindingFill)对于 self.items(pos) 中的项目:如果 mask.contains(pos):# 忽略其他人隐藏的对象返回if isinstance(item, ControlPoint):归还物品如果不是 isinstance(item, Connection):mask.addPath(item.shape().translated(item.scenePos()))def mousePressEvent(self, event):如果 event.button() == QtCore.Qt.LeftButton:item = self.controlPointAt(event.scenePos())如果项目:self.startItem = 项目self.newConnection = Connection(item, event.scenePos())self.addItem(self.newConnection)返回super().mousePressEvent(事件)def mouseMoveEvent(self, event):如果 self.newConnection:item = self.controlPointAt(event.scenePos())if (item and item != self.startItem andself.startItem.onLeft != item.onLeft):p2 = item.scenePos()别的:p2 = event.scenePos()self.newConnection.setP2(p2)返回super().mouseMoveEvent(事件)def mouseReleaseEvent(self, event):如果 self.newConnection:item = self.controlPointAt(event.scenePos())如果 item 和 item != self.startItem:self.newConnection.setEnd(item)如果 self.startItem.addLine(self.newConnection):item.addLine(self.newConnection)别的:# 如果连接存在,则删除连接;删除以下内容# 如果不需要此功能,则行self.startItem.removeLine(self.newConnection)self.removeItem(self.newConnection)别的:self.removeItem(self.newConnection)self.startItem = self.newConnection = 无super().mouseReleaseEvent(事件)定义主():导入系统app = QtWidgets.QApplication(sys.argv)场景 = 场景()scene.addItem(CustomItem(left=True))scene.addItem(CustomItem(left=True))scene.addItem(CustomItem(right=True))scene.addItem(CustomItem(right=True))视图 = QtWidgets.QGraphicsView(scene)view.setRenderHints(QtGui.QPainter.Antialiasing)视图.show()sys.exit(app.exec_())如果 __name__ == '__main__':主要的()

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

I have some custom items in the scene. I would like to allow the user to connect the two items using the mouse. I checked an answer in this question but there wasn't a provision to let users connect the two points. (Also, note that item must be movable)

Here is a demonstration of how I want it to be:

I want the connection between the two ellipses as shown above

Can I know how this can be done?

解决方案

While the solution proposed by JacksonPro is fine, I'd like to provide a slightly different concept that adds some benefits:

  • improved object structure and control;
  • more reliable collision detection;
  • painting is slightly simplified by making it more object-compliant;
  • better readability (mostly by using less variables and functions);
  • clearer connection creation (the line "snaps" to control points);
  • possibility to have control points on both sides (also preventing connections on the same side) and to remove a connection if already exists (by "connecting" again the same points);
  • connections between multiple control points;
  • it's not longer ;-)

The idea is to have control points that are actual QGraphicsItem objects (QGraphicsEllipseItem) and children of CustomItem.
This not only simplifies painting, but also improves object collision detection and management: there is no need for a complex function to draw the new line, and creating an ellipse that is drawn around its pos ensures that we already know the targets of the line by getting their scenePos(); this also makes it much more easy to detect if the mouse cursor is actually inside a control point or not.

Note that for simplification reasons I set some properties as class members. If you want to create subclasses of the item for more advanced or customized controls, those parameters should be created as instance attributes; in that case, you might prefer to inherit from QGraphicsRectItem: even if you'll still need to override the painting in order to draw a rounded rect, it will make it easier to set its properties (pen, brush and rectangle) and even change them during runtime, so that you only need to access those properties within paint(), while also ensuring that updates are correctly called when Qt requires it.

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()

A small suggestion: I've seen that you have the habit of always creating objects in the paint method, even if those values are normally "hardcoded"; one of the most important aspects of the Graphics View framework is its performance, which can be obviously partially degraded by python, so if you have properties that are constant during runtime (rectangles, pens, brushes) it's usually better to make them more "static", at least as instance attributes, in order to simplify the painting as much as possible.

这篇关于如何通过在它们之间画线来连接两个 QGraphicsItem(使用鼠标)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆