2

I'm trying to change the cursor shape on key event:

  • When i press 'C', i want to display a LineCursor,
  • when i press 'S', i want to display a CrossCursor, and
  • when i press 'N', i want to display the standard ArrowCursor.

The cursor change only if it leave the canvas and return to it, but not if the cursor stay in the canvas. self.update() on the canvas don't work

Here the code to reproduce the problem :

from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import sys

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.setObjectName("MainWindow")
        self.resize(942, 935)
        self.centralwidget = QWidget(self)
        self.centralwidget.setObjectName("centralwidget")
        self.horizontalLayout = QHBoxLayout(self.centralwidget)
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.MainView = QGraphicsView(self.centralwidget)
        self.MainView.setObjectName("MainView")
        self.horizontalLayout.addWidget(self.MainView)
        self.setCentralWidget(self.centralwidget)
        self.setWindowTitle("MainWindow")

        self.scene = QGraphicsScene( 0.,0., 1240., 1780. )
        self.canvas = Canvas()

        self.widget = QWidget()
        box_layout = QVBoxLayout()
        self.widget.setLayout(box_layout)
        box_layout.addWidget(self.canvas)
        self.scene.addWidget(self.widget)
        self.MainView.setScene(self.scene)
        self.MainView.setRenderHints(QPainter.Antialiasing)
        self.MainView.fitInView(0, 0, 45, 55, Qt.KeepAspectRatio)

        self.show()

        empty = QPixmap(1240, 1748)
        empty.fill(QColor(Qt.white))
        self.canvas.newPixmap(empty)

    def keyPressEvent(self, e):
        key = e.key()
        if key == Qt.Key_C:
            self.canvas.setCutCursor()
        elif key == Qt.Key_N:
            self.canvas.setNormalCursor()
        elif key == Qt.Key_S:
            self.canvas.setSelectionCursor()


class Canvas(QLabel):
    def __init__(self):
        super().__init__()
        sizePolicy = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
        self.setSizePolicy(sizePolicy)
        self.setAlignment(Qt.AlignLeft)
        self.setAlignment(Qt.AlignTop)

    def newPixmap(self, pixmap):
        self.setPixmap(pixmap)

    def setCutCursor(self):
        newCursor = QPixmap(500,3)
        newCursor.fill(QColor("#000000"))
        self.setCursor(QCursor(newCursor))

    def setSelectionCursor(self):
        self.setCursor(Qt.CrossCursor)

    def setNormalCursor(self):
        self.setCursor(QCursor(Qt.ArrowCursor))


if __name__ == '__main__':
    app = QApplication(sys.argv)
    mainWindow = MainWindow()
    sys.exit(app.exec_())

1 Answer 1

2

It seems to be an old bug that was never resolved: setCursor on QGraphicsView don't work when add QWidget on the QGraphicsScene

There is a possible workaround, but it's far from perfect.
First of all, you have to consider that while dealing with a QGraphicsScene and its view[s] is not easy when dealing with mouse events and widget proxies, mostly because of the multiple nested levels of events and interaction between the actual view (and its parent, up to the top level window) and the proxy itself, which is an abstraction of the widget you added to the scene. While Qt devs did a huge amount of work to make it as transparent as possible, at some point you will probably face some unexpected or undesired behavior that is usually hard to fix or work around, and that's also because a graphics scene might be visualized in more than a single view.

Besides the aforementioned bug, you have to consider that a graphics view uses QWidget.setCursor internally whenever any of its items call setCursor on themselves, and since the view is a very complex widget, at some point it might even try to "restore" the cursor if it thinks it should (even if it shouldn't).
Finally, some events which also have something to do with focus might become in the way of all that.

The first workaround is to set the cursor to the view itself (or, better, the view's viewport, which is the actual widget that shows the scene contents). To ensure that, we obviously need to check if the cursor is inside the canvas.

Unfortunately, because of the event handling written above, this could become a bit messy, as some events are even delayed by at least a cycle within the main Qt event loop; the result is that while setting a cursor the first time might work, setting it again might not, and even if it would, it's possible that the cursor will not be applied until the mouse is moved at least by one pixel.
As a second workaround, we need an event filter to bypass all that and check the cursor whenever the mouse is moved within the viewport margins.

class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        # ...
        self.show()

        empty = QPixmap(1240, 1748)
        empty.fill(QColor(Qt.darkGray))
        self.canvas.newPixmap(empty)

        # install an event filter on the view's viewport;
        # this is very, *VERY* important: on the *VIEWPORT*!
        # if you install it on the view, it will *not* work
        self.MainView.viewport().installEventFilter(self)

    def insideCanvasRect(self, pos):
        canvasRect = self.canvas.rect()
        # translate the canvas rect to its top level window to get the actual
        # geometry according to the scene; we can't use canvas.geometry(), as
        # geometry() is based on the widget's parent coordinates, and that
        # parent could also have any number of parents in turn;
        canvasRect.translate(self.canvas.mapTo(self.canvas.window(), QPoint(0, 0)))
        # map the geometry to the view's transformation, which probably uses
        # some scaling, but also translation *and* shearing; the result is a
        # polygon, as with shearing you could transform a rectangle to an
        # irregular quadrilateral
        polygon = self.MainView.mapFromScene(QRectF(canvasRect))
        # tell if the point is within the resulting polygon
        return polygon.containsPoint(pos, Qt.WindingFill)

    def eventFilter(self, source, event):
        if source == self.MainView.viewport() and (
            (event.type() == QEvent.MouseMove and not event.buttons()) or
            (event.type() == QEvent.MouseButtonRelease)
            ):
                # process the event
                super(MainWindow, self).eventFilter(source, event)
                if self.insideCanvasRect(event.pos()):
                    source.setCursor(self.canvas.cursor())
                else:
                    source.unsetCursor()
                # usually a mouse move event within the view's viewport returns False,
                # but in that case the event would be propagated to the parents, up
                # to the top level window, which might reset the *previous* cursor
                # at some point, no matter if we try to avoid that; to prevent that
                # we return True to avoid propagation.
                # Note that this will prevent any upper-level filtering and *could*
                # also create some issues for the drag and drop framework
                if event.type() == QEvent.MouseMove:
                    return True
        return super(MainWindow, self).eventFilter(source, event)

    def keyPressEvent(self, e):
        # send the canvas a fake leave event
        QApplication.sendEvent(self.canvas, QEvent(QEvent.Leave))
        key = e.key()
        if key == Qt.Key_C:
            self.canvas.setCutCursor()
        elif key == Qt.Key_N:
            self.canvas.setNormalCursor()
        elif key == Qt.Key_S:
            self.canvas.setSelectionCursor()
        pos = self.canvas.rect().center()
        event = QEnterEvent(pos, self.canvas.mapTo(self.canvas.window(), pos), self.canvas.mapToGlobal(pos))
        # send a fake enter event (mapped to the center of the widget, just to be sure)
        QApplication.sendEvent(self.canvas, event)
        # if we're inside the widget, set the view's cursor, otherwise it will not
        # be set until the mouse is moved
        if self.insideCanvasRect(self.MainView.viewport().mapFromGlobal(QCursor.pos())):
            self.MainView.viewport().setCursor(self.canvas.cursor())
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for your answer musicamante. Your solution is working.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.