如何在不冻结 UI 的情况下使用 QProcess 循环的输出更新 UI?

Posted

技术标签:

【中文标题】如何在不冻结 UI 的情况下使用 QProcess 循环的输出更新 UI?【英文标题】:How to update UI with output from QProcess loop without the UI freezing? 【发布时间】:2019-11-07 02:11:24 【问题描述】:

我想要一个通过 QProcess 处理的命令列表,并将其输出附加到我拥有的文本字段中。我发现这两个页面似乎可以完成我需要的每一件事(更新 UI,而不是通过 QThread 冻结 UI):

Printing QProcess Stdout only if it contains a Substring

https://nikolak.com/pyqt-threading-tutorial/

所以我尝试将这两者结合起来......

import sys
from PySide import QtGui, QtCore

class commandThread(QtCore.QThread):
    def __init__(self):
        QtCore.QThread.__init__(self)
        self.cmdList = None
        self.process = QtCore.QProcess()

    def __del__(self):
        self.wait()

    def command(self):
        # print 'something'
        self.process.start('ping', ['127.0.0.1'])
        processStdout = str(self.process.readAll())
        return processStdout

    def run(self):
        for i in range(3):
            messages = self.command()
            self.emit(QtCore.SIGNAL('dataReady(QString)'), messages)
            # self.sleep(1)

class MainWindow(QtGui.QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        self.initUI()


    def dataReady(self,outputMessage):
        cursorOutput = self.output.textCursor()
        cursorSummary = self.summary.textCursor()

        cursorOutput.movePosition(cursorOutput.End)
        cursorSummary.movePosition(cursorSummary.End)
        # Update self.output
        cursorOutput.insertText(outputMessage)

        # Update self.summary
        for line in outputMessage.split("\n"):
            if 'TTL' in line:
                cursorSummary.insertText(line)


        self.output.ensureCursorVisible()
        self.summary.ensureCursorVisible()


    def initUI(self):
        layout = QtGui.QHBoxLayout()
        self.runBtn = QtGui.QPushButton('Run')
        self.runBtn.clicked.connect(self.callThread)

        self.output = QtGui.QTextEdit()
        self.summary = QtGui.QTextEdit()

        layout.addWidget(self.runBtn)
        layout.addWidget(self.output)
        layout.addWidget(self.summary)

        centralWidget = QtGui.QWidget()
        centralWidget.setLayout(layout)
        self.setCentralWidget(centralWidget)


        # self.process.started.connect(lambda: self.runBtn.setEnabled(False))
        # self.process.finished.connect(lambda: self.runBtn.setEnabled(True))

    def callThread(self):
        self.runBtn.setEnabled(False)
        self.get_thread = commandThread()
        # print 'this this running?'
        self.connect(self.get_thread, QtCore.SIGNAL("dataReady(QString)"), self.dataReady)
        self.connect(self.get_thread, QtCore.SIGNAL("finished()"), self.done)

    def done(self):
        self.runBtn.setEnabled(True)


def main():
    app = QtGui.QApplication(sys.argv)
    mainWindow = MainWindow()
    mainWindow.show()
    sys.exit(app.exec_())


if __name__ == '__main__':
    main()

问题是,一旦我单击“运行”按钮,右侧的文本字段似乎没有填充,并且我不再收到任何错误,所以我不确定发生了什么。

我也尝试引用此页面,但我认为我已经在模仿它所描述的内容......?

https://www.qtcentre.org/threads/46056-QProcess-in-a-loop-works-but

最终我想要构建的是一个主窗口,通过 subprocess/QProcess 提交一系列命令,并打开一个小日志窗口,通过显示控制台输出不断更新它的进度。类似于您在安装程序包中看到的内容...

我觉得我离答案如此接近,却又如此遥远。有没有人可以插话?

编辑:所以要回答 eyllanesc 的问题,命令列表必须在前一个命令完成后运行一个,因为我计划使用的命令将占用大量 CPU,而且我不能拥有多个进程跑步。每个命令完成的时间也会完全不同,所以我不能像 time.sleep() 那样随意保持,因为有些命令可能比其他命令完成得更快/更慢。所以理想情况下,弄清楚进程何时完成应该启动另一个命令(这就是为什么我在这个例子中有一个 for 循环来表示它)。

我还决定使用线程,因为显然这是防止 UI 在进程运行时冻结的一种方法,所以我认为我需要利用它在文本字段中进行某种实时提要/更新。

另一件事是在 UI 中,理想情况下,除了使用控制台日志更新文本字段之外,我还希望它具有某种可以更新的标签,上面写着“完成 10 个作业中的 2 个”。所以是这样的:

如果在处理新命令之前可以将自定义消息附加到指示正在运行的命令的文本字段中,那也很好......

更新:很抱歉花了这么长时间发布更新,但根据 eyllanesc 的回答,我能够弄清楚如何打开一个单独的窗口并运行“ping”命令。这是我为在主应用程序中实现结果而编写的示例代码:

from PySide import QtCore, QtGui


class Task:
    def __init__(self, program, args=None):
        self._program = program
        self._args = args or []

    @property
    def program(self):
        return self._program

    @property
    def args(self):
        return self._args


class SequentialManager(QtCore.QObject):
    started = QtCore.Signal()
    finished = QtCore.Signal()
    progressChanged = QtCore.Signal(int)
    dataChanged = QtCore.Signal(str)
    #^ this is how we can send a signal and can declare what type
    # of information we want to pass with this signal

    def __init__(self, parent=None):
        super(SequentialManager, self).__init__(parent)

        self._progress = 0
        self._tasks = []
        self._process = QtCore.QProcess(self)
        self._process.setProcessChannelMode(QtCore.QProcess.MergedChannels)
        self._process.finished.connect(self._on_finished)
        self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)

    def execute(self, tasks):
        self._tasks = iter(tasks)
        #this 'iter()' method creates an iterator object
        self.started.emit()
        self._progress = 0
        self.progressChanged.emit(self._progress)
        self._execute_next()

    def _execute_next(self):
        try:
            task = next(self._tasks)
        except StopIteration:
            return False
        else:
            self._process.start(task.program, task.args)
            return True

    # QtCore.Slot()
    #^ we don't need this line here

    def _on_finished(self):
        self._process_task()
        if not self._execute_next():
            self.finished.emit()

    # @QtCore.Slot()
    def _on_readyReadStandardOutput(self):
        output = self._process.readAllStandardOutput()
        result = output.data().decode()
        self.dataChanged.emit(result)

    def _process_task(self):
        self._progress += 1
        self.progressChanged.emit(self._progress)


class MainWindow(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.outputWindow = outputLog(parentWindow=self)

        self._button = QtGui.QPushButton("Start")

        central_widget = QtGui.QWidget()
        lay = QtGui.QVBoxLayout(central_widget)
        lay.addWidget(self._button)
        self.setCentralWidget(central_widget)

        self._button.clicked.connect(self.showOutput)


    def showOutput(self):
        self.outputWindow.show()
        self.outputWindow.startProcess()

    @property
    def startButton(self):
        return self._button

class outputLog(QtGui.QWidget):
    def __init__(self, parent=None, parentWindow=None):
        QtGui.QWidget.__init__(self,parent)
        self.parentWindow = parentWindow
        self.setWindowTitle('Render Log')
        self.setMinimumSize(225, 150)

        self.renderLogWidget = QtGui.QWidget()
        lay = QtGui.QVBoxLayout(self.renderLogWidget)

        self._textedit = QtGui.QTextEdit(readOnly=True)
        self._progressbar = QtGui.QProgressBar()
        self._button = QtGui.QPushButton("Close")
        self._button.clicked.connect(self.windowClose)
        lay.addWidget(self._textedit)
        lay.addWidget(self._progressbar)
        lay.addWidget(self._button)
        self._manager = SequentialManager(self)

        self.setLayout(lay)

    def startProcess(self):
        self._manager.progressChanged.connect(self._progressbar.setValue)
        self._manager.dataChanged.connect(self.on_dataChanged)
        self._manager.started.connect(self.on_started)
        self._manager.finished.connect(self.on_finished)

        self._progressbar.setFormat("%v/%m")
        self._progressbar.setValue(0)
        tasks = [
            Task("ping", ["8.8.8.8"]),
            Task("ping", ["8.8.8.8"]),
            Task("ping", ["8.8.8.8"]),
            Task("ping", ["8.8.8.8"]),
            Task("ping", ["8.8.8.8"]),
            Task("ping", ["8.8.8.8"]),
        ]
        self._progressbar.setMaximum(len(tasks))
        self._manager.execute(tasks)

    @QtCore.Slot()
    def on_started(self):
        self._button.setEnabled(False)
        self.parentWindow.startButton.setEnabled(False)

    @QtCore.Slot()
    def on_finished(self):
        self._button.setEnabled(True)

    @QtCore.Slot(str)
    def on_dataChanged(self, message):
        if message:
            cursor = self._textedit.textCursor()
            cursor.movePosition(QtGui.QTextCursor.End)
            cursor.insertText(message)
            self._textedit.ensureCursorVisible()

    def windowClose(self):
        self.parentWindow.startButton.setEnabled(True)
        self.close()


if __name__ == "__main__":
    import sys

    app = QtGui.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

我仍然不太了解 QtCore.Slot() 装饰器的使用,因为当我将它们注释掉时,它似乎并没有真正改变结果。但为了安全起见,我把它们放在里面。

【问题讨论】:

我不认为需要使用线程,另一方面你说你有一个命令列表。这些命令应该按顺序执行还是并行执行?如果正在执行一个命令,是否应该执行另一个命令?解释那些重要的细节,也许一个真实的例子可以帮助你更好地了解自己。 嗨,我已经更新了这个问题,提供了更多细节...... 【参考方案1】:

在这种情况下不需要使用线程,因为 QProcess 是使用事件循环执行的。过程是启动一个任务,等待完成信号,获取结果,发送结果,执行下一个任务,直到所有任务完成。解决方案的关键是使用信号并通过迭代器分配任务。

综合以上,解决办法是:

from PySide import QtCore, QtGui


class Task:
    def __init__(self, program, args=None):
        self._program = program
        self._args = args or []

    @property
    def program(self):
        return self._program

    @property
    def args(self):
        return self._args


class SequentialManager(QtCore.QObject):
    started = QtCore.Signal()
    finished = QtCore.Signal()
    progressChanged = QtCore.Signal(int)
    dataChanged = QtCore.Signal(str)

    def __init__(self, parent=None):
        super(SequentialManager, self).__init__(parent)

        self._progress = 0
        self._tasks = []
        self._process = QtCore.QProcess(self)
        self._process.setProcessChannelMode(QtCore.QProcess.MergedChannels)
        self._process.finished.connect(self._on_finished)
        self._process.readyReadStandardOutput.connect(self._on_readyReadStandardOutput)

    def execute(self, tasks):
        self._tasks = iter(tasks)
        self.started.emit()
        self._progress = 0
        self.progressChanged.emit(self._progress)
        self._execute_next()

    def _execute_next(self):
        try:
            task = next(self._tasks)
        except StopIteration:
            return False
        else:
            self._process.start(task.program, task.args)
            return True

    QtCore.Slot()

    def _on_finished(self):
        self._process_task()
        if not self._execute_next():
            self.finished.emit()

    @QtCore.Slot()
    def _on_readyReadStandardOutput(self):
        output = self._process.readAllStandardOutput()
        result = output.data().decode()
        self.dataChanged.emit(result)

    def _process_task(self):
        self._progress += 1
        self.progressChanged.emit(self._progress)


class MainWindow(QtGui.QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)

        self._button = QtGui.QPushButton("Start")
        self._textedit = QtGui.QTextEdit(readOnly=True)
        self._progressbar = QtGui.QProgressBar()

        central_widget = QtGui.QWidget()
        lay = QtGui.QVBoxLayout(central_widget)
        lay.addWidget(self._button)
        lay.addWidget(self._textedit)
        lay.addWidget(self._progressbar)
        self.setCentralWidget(central_widget)

        self._manager = SequentialManager(self)

        self._manager.progressChanged.connect(self._progressbar.setValue)
        self._manager.dataChanged.connect(self.on_dataChanged)
        self._manager.started.connect(self.on_started)
        self._manager.finished.connect(self.on_finished)
        self._button.clicked.connect(self.on_clicked)

    @QtCore.Slot()
    def on_clicked(self):
        self._progressbar.setFormat("%v/%m")
        self._progressbar.setValue(0)
        tasks = [
            Task("ping", ["8.8.8.8"]),
            Task("ping", ["8.8.8.8"]),
            Task("ping", ["8.8.8.8"]),
        ]
        self._progressbar.setMaximum(len(tasks))
        self._manager.execute(tasks)

    @QtCore.Slot()
    def on_started(self):
        self._button.setEnabled(False)

    @QtCore.Slot()
    def on_finished(self):
        self._button.setEnabled(True)

    @QtCore.Slot(str)
    def on_dataChanged(self, message):
        if message:
            cursor = self._textedit.textCursor()
            cursor.movePosition(QtGui.QTextCursor.End)
            cursor.insertText(message)
            self._textedit.ensureCursorVisible()


if __name__ == "__main__":
    import sys

    app = QtGui.QApplication(sys.argv)
    w = MainWindow()
    w.show()
    sys.exit(app.exec_())

【讨论】:

你好。谢谢回答。我担心的是 yoi 提供的这个示例,这会在命令运行时更新文本字段吗?由于我想要最终运行的命令的性质,我不能让用户“等待”进程完成,否则他们将盯着一个空白字段,直到该进程完成。理想情况下,当在 shell 中打印输出/错误行时,应该将信息实时输入到文本字段中 @user3696118 你说的对,我已经修好了,试试看。 您好,感谢您更新您的答案。我已经对此进行了测试,它似乎有效。我对这一切总体上都在做什么有疑问。我查看了“@”符号并了解到它们是“装饰器”,但我仍然不太了解它们,也不了解它在这种情况下背后的意图。另外我不明白你是如何使用 QtCore.Slot() 的,以及你如何只调用它一次,但能够使用使用该方法的装饰器...... @user3696118 mmm, 1) 继续查看装饰器教程,但它们的主要目的是以简单的方式向可调用对象(方法、类、函数等)添加功能。 SO 不是替代教程的地方,因为我们的空间很小,而且它不是该站点的目标。 2) 在装饰器@QtCore.Slot() 的情况下,使用 C++ 建立连接(请记住,PySide 是用 C++ 编写的 Qt Python 的绑定),使方法调用快速且消耗很少的内存。欲了解更多信息,请阅读wiki.qt.io/Qt_for_Python_Signals_and_Slots @user3696118 有什么反馈吗?

以上是关于如何在不冻结 UI 的情况下使用 QProcess 循环的输出更新 UI?的主要内容,如果未能解决你的问题,请参考以下文章

如何在不冻结 gui 的情况下运行 QProcess 的同步链?

如何在不冻结 GUI 的情况下在单个插槽中实现阻塞进程?

如何在不冻结 GUI 的情况下使用视频帧不断更新 contentcontrol?

PyQt:如何在不冻结 GUI 的情况下更新进度?

没有 ui 冻结的 Task.Factory.StartNew 延迟

如何在不冻结线程的情况下延迟答案? [复制]