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?
For this, you might have to implement your own scene class by inheriting QGraphicsScene and overriding the mouse events.
Here is the code which you may improve:
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()
Explanation of the above code:
I make my own custom Item by inheriting the QGraphicsItem. pointONLeft=False
is to check which side the ellipse is to be drawn. If pointONLeft=True
, then red circle that you see in the question's image will be drawn on the left.
The addLine
, itemChange
and moveLineToCenter
methods are taken from here . I suggest you go through that answer before moving on.
The containsPoint
method inside the CustomItem
checks whether the mouse is inside the circle. This method will be accessed from the custom Scene
, if the mouse is inside the circle it will disable the movement by using CustomiItem.setFlag(CustomItem.ItemIsMovable, False)
.
To draw the line I use the QLineF
provided by PyQt. If you want to know how to draw a straight line by dragging I suggest you refer this . while the explanation is for qpainterpath
same can be applied here.
The collidingItems()
is a method provided by QGraphicsItem
. It returns all the items that are colliding including the line itself. So, I created the filterCollidingItems
to filter out only the items that are instances of CustomItem
.
(Also, note that collidingItems()
returns the colliding items in the reverse order they are inserted i,e if CustomItem1 is inserted first and CustomItem second then if the line collides the second item will be returned first. So if two items are on each other and the line is colliding then the last inserted item will become item2
you can change this by changing the z value
)
Readers can add suggestions or queries in the comments. If you have a better answer, feel free to write.
While the solution proposed by JacksonPro is fine, I'd like to provide a slightly different concept that adds some benefits:
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.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.