PyQt进度条

12

使用以下代码后,我的应用程序在几秒钟后卡住了。

我所说的卡住是指挂起。我从Windows获得一个窗口,询问等待还是强制关闭。

我可以补充的是,只有当我单击进度条窗口内部或单击窗口外部使其失去焦点时,才会发生这种情况。如果我启动示例并且不触摸任何内容,则它的工作方式就像应该一样。

from PyQt4 import QtCore
from PyQt4 import QtGui


class ProgressBar(QtGui.QWidget):
    def __init__(self, parent=None, total=20):
        super(ProgressBar, self).__init__(parent)
        self.name_line = QtGui.QLineEdit()

        self.progressbar = QtGui.QProgressBar()
        self.progressbar.setMinimum(1)
        self.progressbar.setMaximum(total)

        main_layout = QtGui.QGridLayout()
        main_layout.addWidget(self.progressbar, 0, 0)

        self.setLayout(main_layout)
        self.setWindowTitle("Progress")

    def update_progressbar(self, val):
        self.progressbar.setValue(val)   

像这样使用:

app = QtGui.QApplication(sys.argv)
bar = ProgressBar(total=101)
bar.show()

for i in range(2,100):
    bar.update_progressbar(i)
    time.sleep(1)

感谢任何帮助。

2个回答

13

您需要在循环运行时允许事件处理,以使应用程序保持响应。

更重要的是,对于长时间运行的任务,您需要提供一种方法让用户在开始后停止循环。

一种简单的方法是使用计时器启动循环,然后在循环运行时定期调用qApp.processEvents

这是一个执行此操作的演示脚本:

import sys, time
from PyQt4 import QtGui, QtCore

class ProgressBar(QtGui.QWidget):
    def __init__(self, parent=None, total=20):
        super(ProgressBar, self).__init__(parent)
        self.progressbar = QtGui.QProgressBar()
        self.progressbar.setMinimum(1)
        self.progressbar.setMaximum(total)
        self.button = QtGui.QPushButton('Start')
        self.button.clicked.connect(self.handleButton)
        main_layout = QtGui.QGridLayout()
        main_layout.addWidget(self.button, 0, 0)
        main_layout.addWidget(self.progressbar, 0, 1)
        self.setLayout(main_layout)
        self.setWindowTitle('Progress')
        self._active = False

    def handleButton(self):
        if not self._active:
            self._active = True
            self.button.setText('Stop')
            if self.progressbar.value() == self.progressbar.maximum():
                self.progressbar.reset()
            QtCore.QTimer.singleShot(0, self.startLoop)
        else:
            self._active = False

    def closeEvent(self, event):
        self._active = False

    def startLoop(self):
        while True:
            time.sleep(0.05)
            value = self.progressbar.value() + 1
            self.progressbar.setValue(value)
            QtGui.qApp.processEvents()
            if (not self._active or
                value >= self.progressbar.maximum()):
                break
        self.button.setText('Start')
        self._active = False

app = QtGui.QApplication(sys.argv)
bar = ProgressBar(total=101)
bar.show()
sys.exit(app.exec_())

更新

假设您使用的是Python的C实现(即CPython),那么解决此问题完全取决于必须与GUI同时运行的任务的性质。更根本地,它由于CPython具有全局解释器锁定(GIL)而确定。

我不打算对CPython的GIL进行任何解释:相反,我将简单推荐观看Dave Beazley的这个出色的PyCon视频,并且就此结束。


通常,在尝试与后台任务并发运行GUI时,要问的第一个问题是:任务是IO限制还是CPU限制?

如果是IO限制(例如访问本地文件系统,从互联网下载等),则解决方案通常非常简单,因为CPython始终会释放GIL以进行I/O操作。后台任务可以异步地完成,或者由工作线程执行,并且不需要执行任何特殊操作以保持GUI响应。

主要困难发生在CPU限制的任务中,此时有第二个问题需要问:该任务是否可以分解成一系列小步骤?

如果可以,则解决方案是定期向GUI线程发送请求,以处理其当前挂起事件的堆栈。上面的演示脚本是此技术的一个简单示例。更通常地,任务将在单独的工作线程中执行,该线程将在完成任务的每个步骤时发出gui-update信号。(注意:确保工作线程永远不会尝试进行任何与GUI相关的操作很重要)。

但是,如果任务不能分解为小步骤,则不会有任何常规线程类型的解决方案有效。GUI将一直冻结,直到任务完成,无论是否使用线程。

对于这种最终情况,唯一的解决方案是使用单独的进程,而不是单独的线程-即利用多处理模块。当然,此解决方案只在目标系统具有多个CPU核心时才有效。如果只有一个CPU核心可供使用,基本上无法做任何事情以提高性能(除了切换到不同的Python实现或完全不同的语言)。


如果我运行这个程序,GUI 窗口也会停止响应,并显示“未响应,强制关闭”错误。但是,如果我等待所有任务完成后,正常的应用程序将继续运行。 - Tuim
1
@Tuim。我的脚本只是基于你问题中的代码的简单演示。它不是一个适用于所有情况的通用解决方案。你需要更新你的问题,用适当的解释说明你想要做什么。这些“任务”是什么?它们是CPU密集型还是IO密集型?执行任务的应用程序是你自己编写的,因此可以修改吗?它是用哪种语言编写的?等等。 - ekhumoro
这些任务是一些安装工作,比如解压zip文件、安装msi/deb包等。但这与本案并不太相关。该应用程序也是用Python编写的,并且完全可定制。同时,我并不希望得到可以复制粘贴的答案!我期望得到一个正确方向的提示,而你给出的提示似乎并不适合我,无意冒犯。 - Tuim
@Tuim。相反地,任务的性质可能是与您的情况相关的唯一事情。请查看我的答案更新。 - ekhumoro
感谢您详细的回答。 - Tuim

0

当你进行GUI编程时,你需要使用某种形式的多线程,你会有一个工作线程和一个更新GUI并响应鼠标和键盘事件的线程。有许多方法可以做到这一点。我建议你看一下这个QProgressBar教程,它有一个非常好的工作示例,展示了如何使用QProgressBar。

在教程中,他们使用了QBasicTimer,这是一种将控制权交还给主线程以便它能够响应GUI事件的方法。通过在你的代码中使用time.sleep,你正在阻塞唯一运行的线程一秒钟。


我知道这一点。但是他们没有像我在示例中那样再利用线程。 - Tuim
1
@Tuim:是的,不要使用 time.sleep() - Junuxx
@Tuim 我更新了答案,进一步解释为什么你的代码和教程不是在做同样的事情。 - Marwan Alsabbagh
我认为在此提及线程只会让事情更加混乱。这里没有涉及到线程,只是关于将控制权交还给Qt事件循环,以便应用程序能够保持响应性。将工作移动到单独的线程上是实现这一目标的策略之一,但这并不是必需的。 - Dan Milburn
也许我需要添加更多的上下文。我有一个控制台应用程序,按顺序执行一系列任务。我只想为此显示一个进度条,由于QT是首选的GUI库,因此我必须使用它。该应用程序除了主线程外没有任何线程模型。所以我只需要一个GUI框架(在新线程中或不在)来显示进度条,并在每个任务完成时更新此进度条。 - Tuim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接