29

I have a simple application that runs a process that can last for several minutes before completing. I am trying to provide an indication to the user that it is processing the request - such as changing the cursor to an hourglass.

But I cannot quite get it to work right. All of my attempts have resulted in either an error or had no effect. And I seem to be calling the cursor shapes incorrectly, since PyQt4.Qt.WaitCursor returns an error that the module does not contain it.

What is the correct way to indicate to the user that the process is running?

General Grievance
  • 4,555
  • 31
  • 31
  • 45
TimothyAWiseman
  • 14,385
  • 12
  • 40
  • 47

7 Answers7

69

I think QApplication.setOverrideCursor is what you're looking for:

PyQt5:

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
...
QApplication.setOverrideCursor(Qt.WaitCursor)
# do lengthy process
QApplication.restoreOverrideCursor()

PyQt4:

from PyQt4.QtCore import Qt
from PyQt4.QtGui import QApplication
...
QApplication.setOverrideCursor(Qt.WaitCursor)
# do lengthy process
QApplication.restoreOverrideCursor()
ekhumoro
  • 115,249
  • 20
  • 229
  • 336
  • 2
    Note: This works in PyQt4. However PySide 1.2.2 under Linux (Ubuntu 16.10) says "X Error: BadCursor (invalid Cursor parameter) 6" "Major opcode: 2 (X_ChangeWindowAttributes)" "Resource id: 0xa". Feeding PySide's setOverrideCursor(Qt.WaitCursor) instead of setOverrideCursor(QCursor(Qt.WaitCursor)) works -- despite the documentation saying the QCursor() is needed. Under some circumstances, there's a similar error for the restore. This appears to be a known PySide bug. – Ubuntourist Dec 04 '16 at 17:38
  • @Ubuntourist. Thanks - I can confirm the bug in PySide. The `QCursor` class has a copy constructor that takes a `Qt.CursorShape` enum value, so it is not really necessary to use `QCursor` itself. I suppose creating the `QCursor` on the Qt side is what fixes the PySide issue. – ekhumoro Dec 04 '16 at 18:00
  • For some reason, this solution works for me in PyQt5 but it takes a focus loss to restore the regular cursor (alt+tab, or moving around other widgets that also change the cursor like any input widget). `QApplication.restoreOverrideCursor()` alone will not do anything if I don't move. – Guimoute Jan 17 '19 at 15:52
  • @Guimoute You should post a new question about this and provide a [mcve]. – ekhumoro Jan 17 '19 at 16:08
21

While Cameron's and David's answers are great for setting the wait cursor over an entire function, I find that a context manager works best for setting the wait cursor for snippets of code:

from contextlib import contextmanager
from PyQt4 import QtCore
from PyQt4.QtGui import QApplication, QCursor

@contextmanager
def wait_cursor():
    try:
        QApplication.setOverrideCursor(QCursor(QtCore.Qt.WaitCursor))
        yield
    finally:
        QApplication.restoreOverrideCursor()

Then put the lengthy process code in a with block:

with wait_cursor():
    # do lengthy process
    pass
dbc
  • 677
  • 8
  • 21
10

ekhumoro's solution is correct. This solution is a modification for the sake of style. I used what ekhumor's did but used a python decorator.

from PyQt4.QtCore import Qt
from PyQt4.QtGui import QApplication, QCursor, QMainWidget

def waiting_effects(function):
    def new_function(self):
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        try:
            function(self)
        except Exception as e:
            raise e
            print("Error {}".format(e.args[0]))
        finally:
            QApplication.restoreOverrideCursor()
    return new_function

I can just put the decorator on any method I would like the spinner to be active on.

class MyWigdet(QMainWidget):

    # ...

    @waiting_effects
    def doLengthyProcess(self):
        # do lengthy process
        pass
Sandy Chapman
  • 11,133
  • 3
  • 58
  • 67
Cameron White
  • 542
  • 4
  • 10
  • 1
    The decorator is a great idea, thanks. I'd recommend calling `function(*args, **kwargs)`, returning `function`'s return value, and wrapping its call in a `try...finally` block for even more goodness. I can't seem to put that in this comment, so next I'll try editing your solution. – Stephan A. Terre Jan 12 '15 at 18:36
  • 1
    Changing the try block to `return function(*args, **kwargs)` might be useful as well, or was in my case at least. – Arthur Endlein Jan 23 '17 at 06:33
1

Better this way:

def waiting_effects(function):
    def new_function(*args, **kwargs):
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        try:
            return function(*args, **kwargs)
        except Exception as e:
            raise e
            print("Error {}".format(e.args[0]))
        finally:
            QApplication.restoreOverrideCursor()
    return new_function
David Miró
  • 2,694
  • 20
  • 20
1

You can use the class QgsTemporaryCursorOverride https://qgis.org/api/classQgsTemporaryCursorOverride.html

But in Python, you should use the Python context manager which is now included in PyQGIS :

from qgis.PyQt.QtCore import Qt
from qgis.utils import OverrideCursor

with OverrideCursor(Qt.WaitCursor):
    do_a_slow(operation)
etrimaille
  • 230
  • 1
  • 4
  • 13
0

The best way to add the cursor according to me would be by using the decorators. By this way you can run any function by just adding the cursor to that function as decorator

import decorator
@decorator.decorator
def showWaitCursor(func, *args, **kwargs):
    QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor)
    try:
        return func(*args, **kwargs)
    finally:
        QtWidgets.QApplication.restoreOverrideCursor()

@showWaitCursor
def youFunc():
    # Long process
0

I find I often need to do:

QApplication.setOverrideCursor(Qt.WaitCursor)
QApplication.ProcessEvents()
...
QApplication.restoreOverrideCursor()

However! During init of the GUI, I've found a modification needs to be made for some reason:

self.ui.show()  # Or your equivalent code to show the widget
QApplication.processEvents()
QApplication.setOverrideCursor(Qt.WaitCursor)
app.processEvents()
...
QApplication.restoreOverrideCursor()

Otherwise the cursor does not "restore" until the mouse passes over a widget that modifies it (like a QLineEdit), similar to what @Guimoute mentioned

Nat
  • 43
  • 6