Search code examples
pythonselectionpysideqgraphicsviewqgraphicsscene

Toggle QPen for selected items after QGraphicsScene selection change


How can I toggle the QPen color for the selected graphicsview items? Ideally I would like to handle this color change in the graphicsview or graphics scene objects rather than directly handling it in the main windows selection event.

Any help is appreciated. Currently it will turn the pen color white when the object is selected. I'm not sure how to turn it back avoiding looping through all objects.

Is there a way i could add a function in the MyGraphicsView class itself that would handle the color change of the pen for any and all selected items in the graph?

Update: More detailed information Here is a list of the selection issues I'm trying to resolve:

  1. When a user clicks and makes a selection rectangle it should deselect all items, restoring them to their default Pen color. The newly selected items should have a white Pen color. If the user is holding control it should add the newly selected items. If items are already selected and the rectangle crosses over them, it should still keep them selected.
  2. If a user clicks in a negative area where no item appears under the cursor, it should clear the current selection.
  3. If a user clicks on an item it should select it and make the Pen color white.
  4. If the user is holding control it should always add to the selection.

enter image description here

enter image description here

Code

import sys
from PySide.QtGui import *
from PySide.QtCore import *
import random


class MyGraphicsView(QGraphicsView):
    def __init__(self):
        super(MyGraphicsView, self).__init__()
        self.setDragMode(QGraphicsView.RubberBandDrag)
        self._isPanning = False
        self._mousePressed = False
        # self.setBackgroundBrush(QImage("C:/Users/jmartini/Desktop/Temp/images/flag_0140.jpg"))
        self.setCacheMode(QGraphicsView.CacheBackground)
        self.setHorizontalScrollBarPolicy( Qt.ScrollBarAlwaysOff )
        self.setVerticalScrollBarPolicy( Qt.ScrollBarAlwaysOff )


    def mousePressEvent(self,  event):
        if event.button() == Qt.LeftButton:
            self._mousePressed = True
            if self._isPanning:
                self.setCursor(Qt.ClosedHandCursor)
                self._dragPos = event.pos()
                event.accept()
            else:
                super(MyGraphicsView, self).mousePressEvent(event)
        elif event.button() == Qt.MiddleButton:
            self._mousePressed = True
            self._isPanning = True
            self.setCursor(Qt.ClosedHandCursor)
            self._dragPos = event.pos()
            event.accept()


    def mouseMoveEvent(self, event):
        if self._mousePressed and self._isPanning:
            newPos = event.pos()
            diff = newPos - self._dragPos
            self._dragPos = newPos
            self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - diff.x())
            self.verticalScrollBar().setValue(self.verticalScrollBar().value() - diff.y())
            event.accept()
        else:
            super(MyGraphicsView, self).mouseMoveEvent(event)


    def mouseReleaseEvent(self, event):
        if event.button() == Qt.LeftButton:
            if self._isPanning:
                self.setCursor(Qt.OpenHandCursor)
            else:
                self._isPanning = False
                self.setCursor(Qt.ArrowCursor)
            self._mousePressed = False
        elif event.button() == Qt.MiddleButton:
            self._isPanning = False
            self.setCursor(Qt.ArrowCursor)
            self._mousePressed = False
        super(MyGraphicsView, self).mouseReleaseEvent(event)


    def mouseDoubleClickEvent(self, event): 
        self.fitInView(self.sceneRect(), Qt.KeepAspectRatio)
        pass


    def keyPressEvent(self, event):
        if event.key() == Qt.Key_Space and not self._mousePressed:
            self._isPanning = True
            self.setCursor(Qt.OpenHandCursor)
        else:
            super(MyGraphicsView, self).keyPressEvent(event)


    def keyReleaseEvent(self, event):
        if event.key() == Qt.Key_Space:
            if not self._mousePressed:
                self._isPanning = False
                self.setCursor(Qt.ArrowCursor)
        else:
            super(MyGraphicsView, self).keyPressEvent(event)


    def wheelEvent(self,  event):
        # zoom factor
        factor = 1.25

        # Set Anchors
        self.setTransformationAnchor(QGraphicsView.NoAnchor)
        self.setResizeAnchor(QGraphicsView.NoAnchor)

        # Save the scene pos
        oldPos = self.mapToScene(event.pos())

        # Zoom
        if event.delta() < 0:
            factor = 1.0 / factor
        self.scale(factor, factor)

        # Get the new position
        newPos = self.mapToScene(event.pos())

        # Move scene to old position
        delta = newPos - oldPos
        self.translate(delta.x(), delta.y())


class MyGraphicsScene(QGraphicsScene):
    def __init__(self,  parent):
        super(MyGraphicsScene,  self).__init__()
        self.setBackgroundBrush(QBrush(QColor(50,50,50)))
        # self.setSceneRect(50,50,0,0)


class MyMainWindow(QMainWindow):
    def __init__(self):
        super(MyMainWindow, self).__init__()
        self.setWindowTitle("Test")
        self.resize(800,600)

        self.gv = MyGraphicsView()
        self.gv.setScene(MyGraphicsScene(self))
        self.setCentralWidget(self.gv)

        self.gv.scene().selectionChanged.connect(self.selection_changed)
        self.populate()


    def populate(self):
        scene = self.gv.scene()

        for i in range(500):
            x = random.randint(0, 1000)
            y = random.randint(0, 1000)
            r = random.randint(2, 8)
            rect = scene.addEllipse(x, y, r, r, QPen(QColor(255,128,0), 0.5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin), QBrush(QColor(255,128,20,128)))
            rect.setFlag( QGraphicsItem.ItemIsSelectable )
            rect.setFlag( QGraphicsItem.ItemIsMovable )

        rect = scene.addEllipse(300, 500, 20, 20, QPen(QColor(255,128,0), 0.5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin), QBrush(QColor(255,0,0,128)))
        rect.setFlag( QGraphicsItem.ItemIsSelectable )
        rect.setFlag( QGraphicsItem.ItemIsMovable )


    def selection_changed(self):
        selection = self.gv.scene().selectedItems()
        print 'Selected:', len(selection)
        for i in selection:
            i.setPen(QPen(QColor(255,255,255), 0.5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))


def main():
    app = QApplication(sys.argv)
    ex = MyMainWindow()
    ex.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

Solution

  • It should be possible to get the required behaviour by caching the last selection and then clearing it before making the new selection:

    class MyGraphicsView(QGraphicsView):
        def __init__(self):
            ...
            self.setScene(MyGraphicsScene(self))
            self.scene().selectionChanged.connect(self.selection_changed)
            self._current_selection = []
    
        def select_items(self, items, on):
            pen = QPen(QColor(255, 255, 255) if on else QColor(255, 128, 0),
                       0.5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
            for item in items:
                item.setPen(pen)
    
        def selection_changed(self):
            try:
                self.select_items(self._current_selection, False)
                self._current_selection = self.scene().selectedItems()
                self.select_items(self._current_selection, True)
            except RuntimeError:
                pass
    

    Here is the complete script:

    import sys
    from PySide.QtGui import *
    from PySide.QtCore import *
    import random
    
    
    class MyGraphicsView(QGraphicsView):
        def __init__(self):
            super(MyGraphicsView, self).__init__()
            self.setDragMode(QGraphicsView.RubberBandDrag)
            self._isPanning = False
            self._mousePressed = False
            self.setCacheMode(QGraphicsView.CacheBackground)
            self.setHorizontalScrollBarPolicy( Qt.ScrollBarAlwaysOff )
            self.setVerticalScrollBarPolicy( Qt.ScrollBarAlwaysOff )
            self.setScene(MyGraphicsScene(self))
            self.scene().selectionChanged.connect(self.selection_changed)
            self._current_selection = []
    
        def select_items(self, items, on):
            pen = QPen(QColor(255, 255, 255) if on else QColor(255, 128, 0),
                       0.5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
            for item in items:
                item.setPen(pen)
    
        def selection_changed(self):
            try:
                self.select_items(self._current_selection, False)
                self._current_selection = self.scene().selectedItems()
                self.select_items(self._current_selection, True)
            except RuntimeError:
                pass
    
        def mousePressEvent(self,  event):
            if event.button() == Qt.LeftButton:
                self._mousePressed = True
                if self._isPanning:
                    self.setCursor(Qt.ClosedHandCursor)
                    self._dragPos = event.pos()
                    event.accept()
                else:
                    super(MyGraphicsView, self).mousePressEvent(event)
            elif event.button() == Qt.MiddleButton:
                self._mousePressed = True
                self._isPanning = True
                self.setCursor(Qt.ClosedHandCursor)
                self._dragPos = event.pos()
                event.accept()
    
    
        def mouseMoveEvent(self, event):
            if self._mousePressed and self._isPanning:
                newPos = event.pos()
                diff = newPos - self._dragPos
                self._dragPos = newPos
                self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() - diff.x())
                self.verticalScrollBar().setValue(self.verticalScrollBar().value() - diff.y())
                event.accept()
            else:
                super(MyGraphicsView, self).mouseMoveEvent(event)
    
    
        def mouseReleaseEvent(self, event):
            if event.button() == Qt.LeftButton:
                if self._isPanning:
                    self.setCursor(Qt.OpenHandCursor)
                else:
                    self._isPanning = False
                    self.setCursor(Qt.ArrowCursor)
                self._mousePressed = False
            elif event.button() == Qt.MiddleButton:
                self._isPanning = False
                self.setCursor(Qt.ArrowCursor)
                self._mousePressed = False
            super(MyGraphicsView, self).mouseReleaseEvent(event)
    
    
        def mouseDoubleClickEvent(self, event):
            self.fitInView(self.sceneRect(), Qt.KeepAspectRatio)
            pass
    
    
        def keyPressEvent(self, event):
            if event.key() == Qt.Key_Space and not self._mousePressed:
                self._isPanning = True
                self.setCursor(Qt.OpenHandCursor)
            else:
                super(MyGraphicsView, self).keyPressEvent(event)
    
    
        def keyReleaseEvent(self, event):
            if event.key() == Qt.Key_Space:
                if not self._mousePressed:
                    self._isPanning = False
                    self.setCursor(Qt.ArrowCursor)
            else:
                super(MyGraphicsView, self).keyPressEvent(event)
    
    
        def wheelEvent(self,  event):
            # zoom factor
            factor = 1.25
    
            # Set Anchors
            self.setTransformationAnchor(QGraphicsView.NoAnchor)
            self.setResizeAnchor(QGraphicsView.NoAnchor)
    
            # Save the scene pos
            oldPos = self.mapToScene(event.pos())
    
            # Zoom
            if event.delta() < 0:
                factor = 1.0 / factor
            self.scale(factor, factor)
    
            # Get the new position
            newPos = self.mapToScene(event.pos())
    
            # Move scene to old position
            delta = newPos - oldPos
            self.translate(delta.x(), delta.y())
    
    
    class MyGraphicsScene(QGraphicsScene):
        def __init__(self,  parent):
            super(MyGraphicsScene,  self).__init__()
            self.setBackgroundBrush(QBrush(QColor(50,50,50)))
            # self.setSceneRect(50,50,0,0)
    
    
    class MyMainWindow(QMainWindow):
        def __init__(self):
            super(MyMainWindow, self).__init__()
            self.setWindowTitle("Test")
            self.resize(800,600)
            self.gv = MyGraphicsView()
            self.setCentralWidget(self.gv)
            self.populate()
    
        def populate(self):
            scene = self.gv.scene()
            for i in range(500):
                x = random.randint(0, 1000)
                y = random.randint(0, 1000)
                r = random.randint(2, 8)
                rect = scene.addEllipse(x, y, r, r, QPen(QColor(255,128,0), 0.5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin), QBrush(QColor(255,128,20,128)))
                rect.setFlag( QGraphicsItem.ItemIsSelectable )
                rect.setFlag( QGraphicsItem.ItemIsMovable )
    
            rect = scene.addEllipse(300, 500, 20, 20, QPen(QColor(255,128,0), 0.5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin), QBrush(QColor(255,0,0,128)))
            rect.setFlag( QGraphicsItem.ItemIsSelectable )
            rect.setFlag( QGraphicsItem.ItemIsMovable )
    
    
    def main():
        app = QApplication(sys.argv)
        ex = MyMainWindow()
        ex.show()
        sys.exit(app.exec_())
    
    
    if __name__ == '__main__':
        main()