Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
843 views
in Technique[技术] by (71.8m points)

python - How to update UI with output from QProcess loop without the UI freezing?

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

enter image description here

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

2.1m questions

2.1m answers

60 comments

57.0k users

...