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.