pie3636
pie3636

Reputation: 814

PyQt QProgressDialog displays as an empty, white window

I have this simple program where I want to have a modal, non-blocking progress window (using a QProgressDialog) that is remotely updated. SIZE simply controls the maximum value of the QProgressDialog. However, if I set it to have a value of 4 or less, the window looks like this during the entire duration of the action:

enter image description here

In other words, the window is completely white and displays no text nor progress bar. If I set the value of SIZE to 5 or more, the display works correctly, but only after the 2-3 first iterations:

enter image description here

and later

enter image description here

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

SIZE = 5

def doGenerate(setValue):
    for x2 in range(SIZE):
        time.sleep(1)
        setValue(x2 + 1)
    print('Done')


class MainMenu(QMainWindow):
    def __init__(self):
        super().__init__()

        self.genAudioButton = QPushButton('Generate', self)
        self.genAudioButton.clicked.connect(self.generate)

        self.setCentralWidget(self.genAudioButton)
        self.show()

    def generate(self):
        try:
            progress = QProgressDialog('Work in progress', '', 0, SIZE, self)
            progress.setWindowTitle("Generating files...")
            progress.setWindowModality(Qt.WindowModal)
            progress.show()
            progress.setValue(0)
            doGenerate(progress.setValue)
        except Exception as e:
            errBox = QMessageBox()
            errBox.setWindowTitle('Error')
            errBox.setText('Error: ' + str(e))
            errBox.addButton(QMessageBox.Ok)
            errBox.exec()
            return

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MainMenu()
    ret = app.exec_()
    sys.exit(ret)

What is causing this and how can I fix it?

Additionally, is there a way to completely remove the cancel button, instead of having an empty button that still cancels the action? The PyQt4 docs (I am using PyQt5) indicate that an empty string should achieve this result, and the C++ docs for Qt5 indicate the same, but that clearly doesn't work here. I haven't found standalone documentation for PyQt5.

Upvotes: 6

Views: 10138

Answers (4)

zeroalpha
zeroalpha

Reputation: 333

I was able to solve the same problem by calling QtGui.QApplication.processEvents() after i set the value to refresh the QProgressDialog

progress.setValue(i)
QApplication.processEvents()

Upvotes: 4

alutian
alutian

Reputation: 11

Almost exactly the problem that led me here. Blank, white dialog, then suddenly it displays correctly but as if 2 or 3 iterations have taken place.

The solution for me makes little sense...

progress = QProgressDialog('Work in progress', '', 0, SIZE, self)
progress.setWindowTitle("Generating files...")
progress.setWindowModality(Qt.WindowModal)
progress.setValue(0)
progress.setValue(1)
progress.setValue(0)

It is almost as if the first setValue gives the blank dialog, and the next two perform the first two iterations so the first real iteration has a correctly displaying dialog to update...

Upvotes: 1

Daniel Farrell
Daniel Farrell

Reputation: 9740

This might be useful to anyone using Quamash/asyncio for async applications.

It takes @eyllanesc example and dispatches a CPU bound task in an executor and removes the dependency on Gtts.

Also for my purpose, I don't know how long the CPU bound will take, so I have set the min and max value of the progress dialog both to zero. This has the nice effect of just animating the progress bar until the task is completed. However, one has to manually call the cancel() method when doing this because the progress dialog cannot know when it is completed. This is done in a callback attached to the future.

def main():

    import sys
    import time
    import quamash
    import asyncio
    import concurrent
    import logging
    import random
    import PyQt5

    # Integrate event loops
    app = PyQt5.QtWidgets.QApplication(sys.argv)
    loop = quamash.QEventLoop(app)
    asyncio.set_event_loop(loop)
    loop.set_debug(False)  # optional

    # Config logging
    logging.basicConfig(level=logging.DEBUG)
    logging.getLogger('quamash').setLevel(logging.ERROR)

    # Print exception before crash!
    def except_hook(cls, exception, traceback):
        sys.__excepthook__(cls, exception, traceback)
    sys.excepthook = except_hook

    class MainWindow(PyQt5.QtWidgets.QMainWindow):
        def __init__(self):
            super().__init__()
            self.exitRequest = asyncio.Event()
            self.genAudioButton = PyQt5.QtWidgets.QPushButton('Generate', self)
            self.genAudioButton.clicked.connect(self.generate)
            self.setCentralWidget(self.genAudioButton)
            self.show()

        def generate(self):
            self.progress = PyQt5.QtWidgets.QProgressDialog('Work in progress...', None, 0, 0, self)
            self.progress.setWindowTitle("Calculation")
            self.progress.setWindowModality(PyQt5.QtCore.Qt.WindowModal)
            self.progress.show()
            self.progress.setValue(0)
            # As the loop to run the coroutine
            loop = asyncio.get_event_loop()
            loop.create_task(self.doGenerate())

        def closeEvent(self, event):
            """ Called when the windows closes.
            """
            self.exitRequest.set()

        def cpuBound(self):
            """ Just wait 2s or raise an exception 50% of the time to test error handling.
            """
            # %50 change of raising an exception
            time.sleep(1.0)
            if random.random() < 0.5:
                time.sleep(1.0)
            else:
                raise RuntimeError(
                    ("If the CPU bound task fails you can raise "
                     "an exception that can be caught and displayed"
                     " like this!")
                )

        def onComplete(self, future):
            """ Callback which contains the future that has completed.
            """

            # Dismiss the progress popup widget before we (possibly)
            # display a popup with an error message.
            self.progress.cancel()

            # Check if we got a result or an exception!
            try:
                result = future.result()
            except Exception as e:
                errBox = PyQt5.QtWidgets.QMessageBox()
                errBox.setWindowTitle('Error')
                errBox.setText('Error: ' + str(e))
                errBox.addButton(PyQt5.QtWidgets.QMessageBox.Ok)
                errBox.exec()  

        async def doGenerate(self):
            """ The coroutine that is added to the event loop when the button is pressed.
            """
            loop = asyncio.get_event_loop()
            with concurrent.futures.ThreadPoolExecutor() as pool:
                future = loop.run_in_executor(pool, self.cpuBound)
                # This call back handles the result or possible exception
                future.add_done_callback(self.onComplete)
                # Block here until complete
                result = await future

    # Startup application
    _window = MainWindow()
    _window.show()
    with loop:
        loop.run_until_complete(_window.exitRequest.wait())

if __name__ == '__main__':
    main()

Upvotes: 2

eyllanesc
eyllanesc

Reputation: 243965

The GUI implements a mainloop through app.exec_(), this loop is used to perform tasks such as checking events, signals, calling some functions, etc. so if we interrupt the loop we can get unexpected behavior like the one you observe. in your case sleep() is a blocking function that should not be used, Qt offers alternatives to it, and one of them is to use a QEventLoop with a QTimer:

def doGenerate(setValue):
    for x2 in range(SIZE):
        loop = QEventLoop()
        QTimer.singleShot(1000, loop.quit)
        loop.exec_()
        setValue(x2 + 1)
    print('Done')

If you want the cancel button not to show, you must pass None:

progress = QProgressDialog('Work in progress', None, 0, SIZE, self)

If you want to use gTTS you must do it through threads, Qt offers several ways to implement it, in this case I will use QThreadPool with QRunnable. We will use the QMetaObject.invokeMethod to update the values of the GUI since Qt prohibits the update of the GUI from another thread that is not from the main thread.

import sys, time
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from gtts import gTTS


class GTTSRunnable(QRunnable):
    def __init__(self, data, progress):
        QRunnable.__init__(self)
        self.data = data
        self.w = progress

    def run(self):
        for i, val in enumerate(self.data):
            text, filename = val
            tts = gTTS(text=text, lang='en')
            tts.save(filename)
            QMetaObject.invokeMethod(self.w, "setValue",
                Qt.QueuedConnection, Q_ARG(int, i+1))
            QThread.msleep(10)

class MainMenu(QMainWindow):
    def __init__(self):
        super().__init__()
        self.genAudioButton = QPushButton('Generate', self)
        self.genAudioButton.clicked.connect(self.generate)
        self.setCentralWidget(self.genAudioButton)
        self.show()

    def generate(self):
        try:
            info = [("hello", "1.mp4"), ("how are you?", "2.mp4"), ("StackOverFlow", "3.mp4")]
            self.progress = QProgressDialog('Work in progress', '', 0, len(info), self)
            self.progress.setWindowTitle("Generating files...")
            self.progress.setWindowModality(Qt.WindowModal)
            self.progress.show()
            self.progress.setValue(0)
            self.doGenerate(info)
        except Exception as e:
            errBox = QMessageBox()
            errBox.setWindowTitle('Error')
            errBox.setText('Error: ' + str(e))
            errBox.addButton(QMessageBox.Ok)
            errBox.exec()
            return

    def doGenerate(self, data):
        self.runnable = GTTSRunnable(data, self.progress)
        QThreadPool.globalInstance().start(self.runnable)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = MainMenu()
    ret = app.exec_()
    sys.exit(ret)

Upvotes: 7

Related Questions