【问题标题】:PyQt QProgressDialog displays as an empty, white windowPyQt QProgressDialog 显示为一个空白的白色窗口
【发布时间】:2017-12-19 02:58:09
【问题描述】:

我有一个简单的程序,我希望在其中有一个远程更新的模态、非阻塞进度窗口(使用 QProgressDialog)。 SIZE 只是控制 QProgressDialog 的最大值。但是,如果我将其设置为 4 或更低的值,则窗口在整个操作期间看起来像这样:

换句话说,窗口是完全白色的,既不显示文本也不显示进度条。如果我将 SIZE 的值设置为 5 或更大,则显示工作正常,但仅在 2-3 次第一次迭代之后:

以后

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)

这是什么原因造成的,我该如何解决?

另外,有没有办法完全删除取消按钮,而不是让一个空按钮仍然取消操作? PyQt4 文档(我正在使用 PyQt5)表明空字符串应该达到这个结果,而 Qt5 的 C++ 文档表明相同,但这显然在这里不起作用。我还没有找到 PyQt5 的独立文档。

【问题讨论】:

    标签: python pyqt modal-dialog pyqt5 nonblocking


    【解决方案1】:

    GUI 通过app.exec_() 实现了一个主循环,该循环用于执行诸如检查事件、信号、调用某些函数等任务。因此,如果我们中断循环,我们会得到与您观察到的一样的意外行为。在您的情况下,sleep() 是一个不应使用的阻塞函数,Qt 提供了替代方法,其中之一是将QEventLoopQTimer 一起使用:

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

    如果你想让取消按钮不显示,你必须通过None:

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

    如果你想使用gTTS,你必须通过线程来实现,Qt 提供了几种方法来实现它,在这种情况下,我将使用QThreadPoolQRunnable。我们将使用QMetaObject.invokeMethod 来更新 GUI 的值,因为 Qt 禁止从非主线程的另一个线程更新 GUI。

    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)
    

    【讨论】:

    • 感谢您的回答。 sleep 是我用来写一个重现问题的最小示例,但我的原始程序实际上是写入 doGenerate 内的文件而不是休眠。这不是通过编写本机 python 文件来完成的,而是使用我几乎无法控制的库中的函数(save 或来自gTTSwrite_to_fp)。我该如何调整您的解决方案?
    • @pie3636 您如何通过 gTT 获得百分比?我是否使用过那个库并且没有找到告诉我进度百分比的函数?
    • 我要生成大量的文件,所以我简单的做的就是在每个文件之后,计算百分比为files_done/total_files。但是在每个文件之后更新 GUI 会导致窗口为空白的问题,因为我想写入文件是阻塞的?
    • 测试我的答案。
    • 完美运行。非常感谢您的帮助,我一个人可能无法解决这个问题。
    【解决方案2】:

    这可能对使用 Quamash/asyncio 进行异步应用程序的任何人有用。

    它以@eyllanesc 为例,在执行器中调度一个 CPU 绑定任务,并消除对 Gtts 的依赖。

    另外,出于我的目的,我不知道 CPU 绑定需要多长时间,所以我将进度对话框的最小值和最大值都设置为零。这具有仅在任务完成之前为进度条设置动画的良好效果。但是,在执行此操作时必须手动调用cancel() 方法,因为进度对话框无法知道它何时完成。这是在附加到未来的回调中完成的。

    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()
    

    【讨论】:

      【解决方案3】:

      几乎正是导致我来到这里的问题。空白的白色对话框,然后突然正确显示,但好像发生了 2 或 3 次迭代。

      解决方案对我来说意义不大......

      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)
      

      就好像第一个 setValue 给出了空白对话框,接下来的两个执行前两次迭代,所以第一次真正的迭代有一个正确显示的对话框来更新......

      【讨论】:

        【解决方案4】:

        在我设置刷新 QProgressDialog 的值后,我可以通过调用 QtGui.QApplication.processEvents() 来解决同样的问题

        progress.setValue(i)
        QApplication.processEvents()
        

        【讨论】:

          猜你喜欢
          • 2020-05-03
          • 2017-06-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多