首页 > 解决方案 > 如何在不冻结 UI 的情况下使用 QProcess 循环的输出更新 UI?

问题描述

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

仅当 QProcess Stdout 包含子字符串时才打印它

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

最终我想要构建的是一个主窗口通过子进程/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() 装饰器的使用,因为当我将它们注释掉时,它似乎并没有真正改变结果。但为了安全起见,我把它们藏在里面。

标签: pythonpython-2.7pyside

解决方案


在这种情况下不需要使用线程,因为 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_())

推荐阅读