如何在不冻结 UI 的情况下使用 QProcess 循环的输出更新 UI? [英] How to update UI with output from QProcess loop without the UI freezing?

查看:75
本文介绍了如何在不冻结 UI 的情况下使用 QProcess 循环的输出更新 UI?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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

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

from PySide import QtCore, QtGui课堂任务:def __init__(self, program, args=None):self._program = 程序self._args = args 或 []@财产定义程序(自己):返回 self._program@财产定义参数(自我):返回 self._args类 SequentialManager(QtCore.QObject):开始 = QtCore.Signal()完成 = QtCore.Signal()progressChanged = QtCore.Signal(int)dataChanged = QtCore.Signal(str)#^ 这是我们如何发送信号并声明什么类型# 我们想通过这个信号传递的信息def __init__(self, parent=None):super(SequentialManager, self).__init__(parent)self._progress = 0self._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执行(自我,任务):self._tasks = iter(tasks)#this 'iter()' 方法创建一个迭代器对象self.started.emit()self._progress = 0self.progressChanged.emit(self._progress)self._execute_next()def _execute_next(self):尝试:任务 = 下一个(self._tasks)除了停止迭代:返回错误别的:self._process.start(task.program, task.args)返回真# QtCore.Slot()#^ 我们这里不需要这一行def _on_finished(self):self._process_task()如果不是 self._execute_next():self.finished.emit()# @QtCore.Slot()def _on_readyReadStandardOutput(self):输出 = self._process.readAllStandardOutput()结果 = output.data().decode()self.dataChanged.emit(result)def_process_task(self):self._progress += 1self.progressChanged.emit(self._progress)类 MainWindow(QtGui.QMainWindow):def __init__(self, parent=None):super(MainWindow, self).__init__(parent)self.outputWindow = outputLog(parentWindow=self)self._button = QtGui.QPushButton(开始")central_widget = QtGui.QWidget()躺 = 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()@财产def startButton(self):返回 self._button类输出日志(QtGui.QWidget):def __init__(self, parent=None, parentWindow=None):QtGui.QWidget.__init__(self,parent)self.parentWindow = parentWindowself.setWindowTitle('渲染日志')self.setMinimumSize(225, 150)self.renderLogWidget = QtGui.QWidget()躺 = QtGui.QVBoxLayout(self.renderLogWidget)self._textedit = QtGui.QTextEdit(readOnly=True)self._progressbar = QtGui.QProgressBar()self._button = QtGui.QPushButton(关闭")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)任务 = [任务(ping",[8.8.8.8"]),任务(ping",[8.8.8.8"]),任务(ping",[8.8.8.8"]),任务(ping",[8.8.8.8"]),任务(ping",[8.8.8.8"]),任务(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):如果消息:光标 = self._textedit.textCursor()cursor.movePosition(QtGui.QTextCursor.End)cursor.insertText(消息)self._textedit.ensureCursorVisible()def windowClose(self):self.parentWindow.startButton.setEnabled(True)self.close()如果 __name__ == __main__":导入系统app = QtGui.QApplication(sys.argv)w = 主窗口()w.show()sys.exit(app.exec_())

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

解决方案

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

综合以上,解决办法是:

from PySide import QtCore, QtGui课堂任务:def __init__(self, program, args=None):self._program = 程序self._args = args 或 []@财产定义程序(自己):返回 self._program@财产定义参数(自我):返回 self._args类 SequentialManager(QtCore.QObject):开始 = QtCore.Signal()完成 = QtCore.Signal()progressChanged = QtCore.Signal(int)dataChanged = QtCore.Signal(str)def __init__(self, parent=None):super(SequentialManager, self).__init__(parent)self._progress = 0self._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执行(自我,任务):self._tasks = iter(tasks)self.started.emit()self._progress = 0self.progressChanged.emit(self._progress)self._execute_next()def _execute_next(self):尝试:任务 = 下一个(self._tasks)除了停止迭代:返回错误别的:self._process.start(task.program, task.args)返回真QtCore.Slot()def _on_finished(self):self._process_task()如果不是 self._execute_next():self.finished.emit()@QtCore.Slot()def _on_readyReadStandardOutput(self):输出 = self._process.readAllStandardOutput()结果 = output.data().decode()self.dataChanged.emit(result)def_process_task(self):self._progress += 1self.progressChanged.emit(self._progress)类 MainWindow(QtGui.QMainWindow):def __init__(self, parent=None):super(MainWindow, self).__init__(parent)self._button = QtGui.QPushButton("开始")self._textedit = QtGui.QTextEdit(readOnly=True)self._progressbar = QtGui.QProgressBar()central_widget = QtGui.QWidget()躺 = 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)任务 = [任务(ping",[8.8.8.8"]),任务(ping",[8.8.8.8"]),任务(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):如果消息:光标 = self._textedit.textCursor()cursor.movePosition(QtGui.QTextCursor.End)cursor.insertText(消息)self._textedit.ensureCursorVisible()如果 __name__ == "__main__":导入系统app = QtGui.QApplication(sys.argv)w = 主窗口()w.show()sys.exit(app.exec_())

I am wanting to have a list of commands being processed through a QProcess and have its output be appended to a textfield I have. I've found a these two pages that seems to do each of the things i need (updating the UI, and not freezing the UI via QThread):

Printing QProcess Stdout only if it contains a Substring

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

So i tried to combine these two....

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

The problem is that once I click the "Run" button the textfield on the right doesn't seem to populate, and i am no longer getting any errors so I am not sure what is happening.

I tried referring to this page as well but I think i am already emulating what it is describing...?

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

Ultimately what I want to build is for a main window to submit a series of commands via subprocess/QProcess, and open up a little log window that constantly updates it on the progress via displaying the console output. Similar to what you kind of see in like Installer packages...

I feel like i am so close to an answer, yet so far away. Is anyone able to chime in on this?

EDIT: so to answer eyllanesc's question, the list of commands has to be run one after the previous one has completed, as the command i plan to use will be very CPU intensive, and i cannot have more than one process of it running. also the time of each command completing will completely vary so I can't just have a arbitrary hold like with time.sleep() as some may complete quicker/slower than others. so ideally figuring out when the process has finished should kickstart another command (which is why i have a for loop in this example to represent that).

i also decided to use threads because apparently that was one way of preventing the UI to freeze while the process was running,so i assumed i needed to utilize this to have a sort of live feed/update in the text field.

the other thing is in the UI i would ideally in addition to updating a text field with console logs, i would want it to have some sort of label that gets updated that says something like "2 of 10 jobs completed". so something like this:

It would be nice too when before a new command is being processed a custom message can be appended to the text field indicating what command is being run...

UPDATE: apologies for taking so long to post an update on this, but based on eyllanesc's answer, I was able to figure out how to make this open a separate window and run the "ping" commands. here is the example code I have made to achieve my results in my main application:

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

i still don't really understand the use of the QtCore.Slot() decorators as when I commented them out it didn't really seem to change the result. But i kept them in just to be safe.

解决方案

It is not necessary to use threads in this case since QProcess is executed using the event loop. The procedure is to launch a task, wait for the finishes signal, get the result, send the result, and execute the next task until all the tasks are finished. The key to the solution is to use the signals and distribute the tasks with an iterator.

Considering the above, the solution is:

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

这篇关于如何在不冻结 UI 的情况下使用 QProcess 循环的输出更新 UI?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆