3

I am trying to create a custum animated cursor that replaces the regular cursor for when lengthy processes are taking place similar to here and here, however, I woud like mine to be animated, for example using a gif, just as it is the case with the standard Qt.WaitCursor. How can I do this? I found this solution regarding animated system tray icons, however, I didn't manage to adapt it for it to work with the cursor icon.

As a side note: When I try to execute pm.setAlphaChannel(bm) as stated in the first link it does not work for me and I get the following error:

'AttributeError: QPixmap' object has no attribute 'setAlphaChannel'

Which is odd because according to the documentation, QPixmap does have a setAlphaChannel method.

eyllanesc
  • 235,170
  • 19
  • 170
  • 241
mapf
  • 1,906
  • 1
  • 14
  • 40
  • 1
    What animation do you mean? – eyllanesc Apr 01 '19 at 13:18
  • Thanks for your questions! I added the information. pyqt5 and by animation I mean something like a gif for example. In Windows 10 you have this spinning blue ring for example. I would like to replace that. – mapf Apr 01 '19 at 13:25

1 Answers1

4

One possible solution is to create a class that handles the update of the cursor of a given widget. In the following example the cursor is set when the start button is pressed and the cursor is restored when the stop button is pressed:

from PyQt5 import QtCore, QtGui, QtWidgets

class ManagerCursor(QtCore.QObject):
    def __init__(self, parent=None):
        super(ManagerCursor, self).__init__(parent)
        self._movie = None
        self._widget = None
        self._last_cursor = None

    def setMovie(self, movie):
        if isinstance(self._movie, QtGui.QMovie):
            if not self._movie != QtGui.QMovie.NotRunning:
                self._movie.stop()
            del self._movie
        self._movie = movie
        self._movie.frameChanged.connect(self.on_frameChanged)
        self._movie.started.connect(self.on_started)
        self._movie.finished.connect(self.restore_cursor)

    def setWidget(self, widget):
        self._widget = widget

    @QtCore.pyqtSlot()
    def on_started(self):
        if self._widget is not None:
            self._last_cursor = self._widget.cursor()

    @QtCore.pyqtSlot()
    def restore_cursor(self):
        if self._widget is not None:
            if self._last_cursor is not None:
                self._widget.setCursor(self._last_cursor)
        self._last_cursor = None

    @QtCore.pyqtSlot()
    def start(self):
        if self._movie is not None:
            self._movie.start()

    @QtCore.pyqtSlot()
    def stop(self):
        if self._movie is not None:
            self._movie.stop()
            self.restore_cursor()

    @QtCore.pyqtSlot()
    def on_frameChanged(self):
        pixmap = self._movie.currentPixmap()
        cursor = QtGui.QCursor(pixmap)
        if self._widget is not None:
            if self._last_cursor is None:
                self._last_cursor = self._widget.cursor()
            self._widget.setCursor(cursor)


class Widget(QtWidgets.QWidget):
    def __init__(self, parent=None):
        super(Widget, self).__init__(parent)
        start_btn = QtWidgets.QPushButton("start", clicked=self.on_start)
        stop_btn = QtWidgets.QPushButton("stop", clicked=self.on_stop)

        self._manager = ManagerCursor(self)
        movie = QtGui.QMovie("loading-icon.gif")
        self._manager.setMovie(movie)
        self._manager.setWidget(self)

        lay = QtWidgets.QVBoxLayout(self)
        lay.addWidget(start_btn)
        lay.addWidget(stop_btn)
        lay.addStretch()

    @QtCore.pyqtSlot()
    def on_start(self):
        self._manager.start()

    @QtCore.pyqtSlot()
    def on_stop(self):
        self._manager.stop()

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    w = Widget()
    w.resize(640, 480)
    w.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Ok, in case you read my previous comment, never mind it! I made a mistake in the path to my gif file, so it could never be loaded. I expected an error message in that case though, that's why it took me so long to find it. Interesting that QMovie just ignores an incorrect file path. – mapf Apr 01 '19 at 14:54
  • 1
    @mapf Qt for efficiency reasons do not use the exceptions, and in this case assume that you have created an empty QMovie, if you want to verify if the gif has been loaded you should use the isValid() method: print(movie.isValid()) – eyllanesc Apr 01 '19 at 15:01
  • Good to know! Thanks! – mapf Apr 01 '19 at 15:05
  • sorry if this is nitpicky, but I have a follow-up question: When I use the method described [here](https://stackoverflow.com/questions/8218900/how-can-i-change-the-cursor-shape-with-pyqt), i.e. `QApplication.setOverrideCursor(Qt.WaitCursor)`, the cursor stays the same (as in keeps beeing the rotating cicle) and keeps being updated no matter where you hover over. However, with your method, this is not the case. E.g. if you hover over the frame of the window or a text field the cursor changes, and for some reason, when I load data using pickle (2nd thread), the cursor is not updated. – mapf Apr 02 '19 at 09:26
  • I guess this has something to do with how deep these methods are implemented? Is there a way to 'go deeper', so that the custom cursor behaves like a Qt.WaitCursor as described above? – mapf Apr 02 '19 at 09:28
  • Awesome @eyllanesc! Thank you so much for this code. – K.Mulier May 14 '19 at 14:39
  • Hi, I'd just like to bump my question from above again, since I just got back to trying and figuring this out again. How can I make sure that the waitcursor also works when hovering over secondary windows or text fields? – mapf Nov 01 '19 at 10:23
  • 1
    @mapf If you have another question (which may be similar to the one I answered) then you must create a new publication. – eyllanesc Nov 01 '19 at 10:25
  • Thanks! I just did so: https://stackoverflow.com/questions/59105974/how-to-stop-an-animated-qcursor-from-freezing-when-loading-or-dumping-with-pickl – mapf Nov 29 '19 at 13:43