I’m trying to mimick path editing similar to what you would see in photoshop, which interacts this way…
- You select the Path and it’s Points become visible
- Users can click and drag any Point item of the Path, to adjust the Path
- When users click and drag the Path directly it moves the Path
- When the Path is deselected the Points become hidden again
Where I’m having issues are
- Making the Points hidden when the Path is deselected but not hidden when a Point of the selected spline is being Edited
Here is what i have:
Here is a reference to something I’m trying to match:
import sys
import math
import random
from PySide2 import QtWidgets, QtGui, QtCore
# SETTINGS
handle_size = 16
handle_color = QtGui.QColor(40,130,230)
handle_radius = 8
class AnnotationPointItem(QtWidgets.QGraphicsEllipseItem):
def __init__(self, positionFlag=0, pos=QtCore.QPointF(), parent=None):
super(AnnotationPointItem, self).__init__(-handle_radius, -handle_radius, 2*handle_radius, 2*handle_radius, parent)
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable | QtWidgets.QGraphicsItem.ItemSendsScenePositionChanges)
self.setPen(QtGui.QPen(handle_color, 4, QtCore.Qt.SolidLine))
self.setBrush(QtGui.QBrush(QtGui.QColor('white')))
self.positionFlag = positionFlag
def paint(self, painter, option, widget=None):
# Remove the selection outline
# if self.isSelected():
# option.state &= ~QtWidgets.QStyle.State_Selected
super(AnnotationPointItem, self).paint(painter, option, widget)
# def mousePressEvent(self, event):
# # Handle the event, but don't propagate to the parent
# # event.accept()
# print('clicked....')
# return super(AnnotationPointItem, self).mousePressEvent(event)
def itemChange(self, change, value):
# print(change, self.isSelected())
if change == QtWidgets.QGraphicsItem.ItemPositionChange:
# print('ItemPositionChange')
pass
elif change == QtWidgets.QGraphicsItem.ItemPositionHasChanged:
# print('ItemPositionHasChanged')
parent = self.parentItem()
if parent:
# Get the position of the cursor in the view's coordinates
if self.positionFlag == 0:
parent.setPoints(start=self.pos())
elif self.positionFlag == 1:
parent.setPoints(end=self.pos())
elif change == QtWidgets.QGraphicsItem.ItemSelectedChange:
pass
return super(AnnotationPointItem, self).itemChange(change, value)
class AnnotationPathItem(QtWidgets.QGraphicsLineItem):
def __init__(self,
start=QtCore.QPointF(),
end=QtCore.QPointF(),
color=QtCore.Qt.green,
thickness=10,
parent=None):
super(AnnotationPathItem, self).__init__(start.x(), start.y(), end.x(), end.y(), parent)
self._color = color
self._thickness = thickness
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable | QtWidgets.QGraphicsItem.ItemIsSelectable)
self.setPen(QtGui.QPen(self._color, self._thickness, QtCore.Qt.SolidLine))
# child items
self.startPointItem = AnnotationPointItem(positionFlag=0, parent=self)
self.startPointItem.hide()
self.startPointItem.setPos(self.line().p1())
self.endPointItem = AnnotationPointItem(positionFlag=1, parent=self)
self.endPointItem.hide()
self.endPointItem.setPos(self.line().p2())
def itemChange(self, change, value):
if change == QtWidgets.QGraphicsItem.ItemSelectedChange:
self.selectionChanged(value)
return super(AnnotationPathItem, self).itemChange(change, value)
def selectionChanged(self, selected):
# Implement what you want to do when the selection changes
print(self.startPointItem.isSelected(), self.endPointItem.isSelected())
if selected or self.startPointItem.isSelected() or self.endPointItem.isSelected():
self.startPointItem.show()
self.endPointItem.show()
# else:
# self.startPointItem.hide()
# self.endPointItem.hide()
def paint(self, painter, option, widget=None):
# Remove the selection outline
if self.isSelected():
option.state &= ~QtWidgets.QStyle.State_Selected
super(AnnotationPathItem, self).paint(painter, option, widget)
def setPoints(self, start=None, end=None):
currentLine = self.line()
if start != None:
currentLine.setP1(start)
if end != None:
currentLine.setP2(end)
self.setLine(currentLine)
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.resize(1200,1200)
self.scene = QtWidgets.QGraphicsScene(self)
self.scene.setBackgroundBrush(QtGui.QColor(40,40,40))
self.view = QtWidgets.QGraphicsView(self)
self.view.setSceneRect(-4000, -4000, 8000, 8000)
self.view.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform)
self.view.setMouseTracking(True)
self.view.setScene(self.scene)
self.addButton = QtWidgets.QPushButton("Add Annotation", self)
self.addButton.clicked.connect(self.add_annotation)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.view)
layout.addWidget(self.addButton)
self.setLayout(layout)
# samples
item = AnnotationPathItem(QtCore.QPointF(-70, -150), QtCore.QPointF(150, -350))
self.scene.addItem(item)
def add_annotation(self):
r = random.randint(0,255)
g = random.randint(0,255)
b = random.randint(0,255)
color = QtGui.QColor(r,g,b)
startPos = QtCore.QPointF(random.randint(-200,200), random.randint(-200,200))
endPos = QtCore.QPointF(random.randint(-200,200), random.randint(-200,200))
item = AnnotationPathItem(startPos, endPos, color)
self.scene.addItem(item)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()
2
Answers
You cannot achieve this by checking the selection changes of the item, because when an item gets selected upon mouse press, the view has already deselected all the other.
Instead, you can use the
selectionChanged
signal of the scene, which is emitted when all selection changes are completed, and decide whether to show items or not. Since items are usually not created with a scene at first, you have to connect to the signal upon theItemSceneChange
item change:While my other answer is acceptable, I don’t really like the fact that each item is connected to the selection change signal.
In case the scene may potentially contain lots of items (in the order of hundreds or thousands), this means that every time the selection changes the signal will call the connected function of each one of those items, and even unnecessarily if the selection change doesn’t affect any of those item types.
The Graphics View framework is quite fast, but there’s little point in repeating the same calls that result in the same output (such as
scene.selectedItems()
) especially when dealing with the python bottleneck.In such situations, it’s usually preferable to use centralized functions to handle such a common and repeated job, possibly adopting some optimizations to improve its performance and return to the event loop as soon as possible.
Doing that may increase code complexity, but could also result in faster response times for complex scenes with an undefined (and possibly extremely high) item count.
The following alternative uses a custom QGraphicsScene to handle selection changes and child visibility on its own; when the signal is emitted, it only iterates between all known "annotation items", and each item will eventually iterate and change the visibility of its control points only in case the selection change has actually affected them.
I also added a basic
object
subclass intended to be used as a mixin, with the benefit of being able to use it as a primitive class for any annotation-based item that uses control points. With proper implementation, you can also use this with all basic QGraphicsItems, including QGraphicsPathItem.Note the remark about the set comparison: I’ve not done thorough research about the low level implementation, but according to this and this related posts, using
set.isdisjoint()
is a better choice, and despite what the documentation may let assume, which says:As far as I can understand, that can (should?) probably be rephrased as:
Assuming that my understanding is correct,
not set.isdisjoint(other)
is much faster than checking the intersection, and especially for this case:bool()
;isdisjoint()
just returnsFalse
as soon as a common element is found, which is exactly what we need; if no element is found in the selection, it would still returnTrue
, but without the overhead of creating a further set;