Issue
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?
Solution
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.
Answered By - musicamante
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.