Search code examples
pythonpyqtqgraphicsviewqgraphicsscene

Selecting item from QGraphicsScene in PyQt5


I am loading image using QGraphicsView and QGraphicsSCene. I have multiple RectItems in QGraphicsScene. Code is given below;

def load_image(self,image_item):
    rect_list = [[20,30,70,35],[50,100,60,100],[410,450,60,100]]
    self.pic = QPixmap(str(image_item.text()))
    self.scene = QGraphicsScene(self.centralWidget)
    self.brush = QBrush(Qt.BDiagPattern)
    self.pen = QPen(Qt.red)
    self.pen.setWidth(2)
    self.load_view = self.scene.addItem(QGraphicsPixmapItem(self.pic))
    for rect in rect_list:
        self.rect_item = self.scene.addRect(rect[0],rect[1],rect[2],rect[3],self.pen,self.brush) # x,y,w,h
        self.rect_item.setFlag(QGraphicsItem.ItemIsSelectable) #Item is Selectable
        self.rect_item.setFlag(QGraphicsItem.ItemIsMovable) # Item is Movable

    self.gView.setScene(self.scene)
    self.gView.setRenderHint(QPainter.Antialiasing)
    self.gView.show()

Now, When ever I click on one rect_item from the item list in QgraphicsScene, I want to print that item.

Screenshot is given here


Solution

  • I think that the most simple solution, if you really need an "item clicked" message only, is to add Qt.ItemIsFocusable flag to the item and then use the focusItemChanged signal of the scene:

        def load_image(self, image_item):
            # ...
            self.scene = QGraphicsScene(self.centralWidget)
            self.scene.focusItemChanged.connect(self.focusChanged)
            # ...
            for rect in rect_list:
                self.rect_item = self.scene.addRect(rect[0],rect[1],rect[2],rect[3],self.pen,self.brush) # x,y,w,h
                self.rect_item.setFlag(QGraphicsItem.ItemIsSelectable)
                self.rect_item.setFlag(QGraphicsItem.ItemIsMovable)
                # required for focusItemChanged signal to work:
                self.rect_item.setFlag(QGraphicsItem.ItemIsFocusable)
    
        def focusItemChanged(self, newItem, oldItem, reason):
            if newItem and reason == Qt.MouseFocusReason:
                print('item {} clicked!'.format(newItem))
    

    This method has some issues, though, most importantly the fact that if an item is already selected (hence, it has focus) you won't get the signal.

    There's no immediate solution for this, because basic QGraphicsItems are not QObject descendants, meaning that they cannot emit any signal.

    If you don't need signal/slot support, you can subclass QGraphicsRectItem and reimplement its mousePressEvent:

    class ClickableGraphicsRectItem(QGraphicsRectItem):
        def __init__(self, x, y, w, h, pen, brush):
            super(ClickableGraphicsRectItem, self).__init__(x, y, w, h)
            self.setPen(pen)
            self.setBrush(brush)
            # flags can be set all at once using the "|" binary operator
            self.setFlags(self.ItemIsSelectable|self.ItemIsMovable)
    
        def mousePressEvent(self, event):
            super(ClickableGraphicsRectItem, self).mousePressEvent(event)
            if event.button() == Qt.LeftButton:
                print('item clicked!')
    
    class MyProgram(QMainWindow):
        def load_image(self, image_item):
            # ...
            for rect in rect_list:
                self.rect_item = ClickableGraphicsRectItem(...)
    

    If you do need some signal/slot mechanism, you could subclass the scene also and make the item emit its signal. This is not the best practice, but it works :-)

    class ClickableGraphicsRectItem(QGraphicsRectItem):
        # ...
        def mousePressEvent(self, event):
            super(ClickableGraphicsRectItem, self).mousePressEvent(event)
            if event.button() == Qt.LeftButton:
                self.scene().itemClicked.emit(self)
    
    class ItemClickableGraphicsScene(QGraphicsScene):
        itemClicked = pyqtSignal(QGraphicsItem)
    
    class MyProgram(QMainWindow):
        def load_image(self, image_item):
            # ...
            self.scene = ItemClickableGraphicsScene(self.centralWidget)
            self.scene.itemClicked.connect(self.itemClicked)
            # ...
            for rect in rect_list:
                self.rect_item = ClickableGraphicsRectItem(...)
    
        def itemClicked(self, item):
            print('item {} clicked!'.format(item))
    

    Alternatively, you can reimplement the mousePressEvent of the graphics view. In this example I just check if it's a QGraphicsRectItem (since you also have the QGraphicsPixmapItem), but if you add other item types you'll have to find a way to "recognize" them more carefully.

    class ClickableItemView(QGraphicsView):
        def mousePressEvent(self, event):
            super(ClickableItemView, self).mousePressEvent(event)
            if event.button() == Qt.LeftButton:
                item = self.itemAt(event.pos())
                if isinstance(item, QGraphicsRectItem):
                    print('item {} clicked!'.format(item))