Search code examples
pythonpyqtqthreadqprocess

Start QProcess from within QDialog that is used as a progress monitor


I have a main pyqt program that needs to run external program with arguments. I would like to use a QDialog as a sort of a status monitor that would capture the external program's stdout while it is executing and display them in a textbox inside the QDialog. I have the following status monitor code:

class ProgressInfo(QtGui.QDialog):
    def __init__(self, cmd, args, parent=None):
        #super(self).__init__(parent)
        QDialog.__init__(self)
        self.ui = Ui_Dialog()
        self.ui.setupUi(self)
        self.cmd = cmd
        self.args = args
        self.keepGoing = True
        layout = QFormLayout()
        layout.setContentsMargins(10, 10, 10, 10)
        self.output = QtGui.QTextEdit()
        layout.addRow(self.output)
        layout.addRow(self.ui.buttonBox)
        self.setLayout(layout)

        self.ext_process = QtCore.QProcess(self)
        #self.ext_process.waitForFinished(-1)
        #self.ext_process.waitForStarted()
        #self.ext_process.readyRead.connect(self.dataReady)
        self.ext_process.started.connect(self.open)
        self.ext_process.readyReadStandardOutput.connect(self.dataReady)
        self.ext_process.finished.connect(self.onProcessFinished)
        self.ext_process.start(self.cmd, self.args)

    def dataReady(self):
        cursor = self.output.textCursor()
        cursor.movePosition(cursor.End)
        cursor.insertText(str(self.ext_process.readAll()))
        self.output.ensureCursorVisible()

    def onProcessFinished(self):
        cursor = self.output.textCursor()
        cursor.movePosition(cursor.End)
        #cursor.insertText(str(self.ext_process.readAll()))
        cursor.insertText(str(self.ext_process.readAllStandardOutput()))
        self.output.ensureCursorVisible()

Then I would instantiate this with the following command:

prog='C:/Program Files (x86)/My Program/Execute.exe'
margs=['D:/Data/Input1.txt', 'D:/Data/Input2.txt']
status = ProgressInfo(prog, margs, self)

So far this hasn't worked yet.

Problem 1: the external program will run only after I uncommented out the waitForFinished(-1) line.

Problem 2. the QDialog box only open in a flash, then disappears.

Problem 3. obviously, no standout from the running program is displayed.

Lastly, the code I put together draws on many people's ideas and lessons, but I look at it, it seems that it can only print out all of the standout after the program finished, but I was hoping it will display line by line as the program is writing them out at runtime.

My tool chains: Python 64-bit version 2.7.5 and I am developing on Windows 7 box

('Qt version:', '4.8.5')
('SIP version:', '4.14.7')
('PyQt version:', '4.10.2')

Thanks for any help.


Solution

  • Here is an example how you can do it (I use QWidget but you can also use QDialog or whatever). I don't use a separate thread because the UI doesn't need to be interactive. If you want to add buttons etc. then you should consider going for the good old QThread running a QObject model provided by Qt.

    #!/usr/bin/python
    from PyQt4.QtGui import * 
    from PyQt4.QtCore import * 
    import sys
    
    
    class MyQProcess(QWidget):     
      def __init__(self):    
       super(QWidget, self).__init__()
    
       # Add the UI components (here we use a QTextEdit to display the stdout from the process)
       layout = QVBoxLayout()
       self.edit = QTextEdit()
       self.edit.setWindowTitle("QTextEdit Standard Output Redirection")
       layout.addWidget(self.edit)
       self.setLayout(layout)
    
       # Add the process and start it
       self.process = QProcess()
       self.setupProcess()   
    
       # Show the widget
       self.show()
    
      def setupProcess(self):
        # Set the channels
        self.process.setProcessChannelMode(QProcess.MergedChannels)
        # Connect the signal readyReadStandardOutput to the slot of the widget
        self.process.readyReadStandardOutput.connect(self.readStdOutput)
        # Run the process with a given command
        self.process.start("df -h")
    
      def __del__(self):
        # If QApplication is closed attempt to kill the process
        self.process.terminate()
        # Wait for Xms and then elevate the situation to terminate
        if not self.process.waitForFinished(10000):
          self.process.kill()
    
      @pyqtSlot()
      def readStdOutput(self):
        # Every time the process has something to output we attach it to the QTextEdit
        self.edit.append(QString(self.process.readAllStandardOutput()))
    
    
    def main():  
        app = QApplication(sys.argv)
        w   = MyQProcess()
    
        return app.exec_()
    
    if __name__ == '__main__':
        main()
    

    Notice that the command I'm using (df -h) runs once (it's a Linux command which displays the disk usage on your hard drives) and then it's over. You can replace it also with your Execute.exe which can run indefinitely. I have tested it with htop (a terminal-based advanced task manager), which once started doesn't stop unless the user wants it to or the system stops (crash, shutdown etc.).

    Note that you have to ensure that the external process is stopped in a clean manner. This can be done inside __del__ (destructor) or another function invoked at the end of the life of a given widget. What I've done is basically send a SIGTERM (terminate) to the external process and once a given amount of time has passed but the process is still running I elevate the situation to SIGKILL (kill).

    The code needs more work obviously but it should be enough to give you an idea how things work.

    Here is the same version of the code above but with an extra thread. Note that I am redirecting the output from the external process to a slot in my worker. You don't have to do that unless you want to maybe work on that output. So you can skip this and connect your process signal to the slot in your widget that receives it and outputs its content. The processing of the output will be done again inside the separate thread so you can go the distance instead of freezing your UI (which will happen if you follow the

    from PyQt4.QtGui import * 
    from PyQt4.QtCore import * 
    import sys
    
    class Worker(QObject):
      sendOutput = pyqtSignal(QString)  
    
      def __init__(self):
        super(Worker, self).__init__()
        self.process = QProcess()
        self.setupProcess()
    
      def __del__(self):
        self.process.terminate()
        if not self.process.waitForFinished(10000):
          self.process.kill()
    
      def setupProcess(self):
        self.process.setProcessChannelMode(QProcess.MergedChannels)
        self.process.readyReadStandardOutput.connect(self.readStdOutput)
        self.process.start("htop")
    
      @pyqtSlot()
      def readStdOutput(self):
        output = QString(self.process.readAllStandardOutput())
        # Do some extra processing of the output here if required
        # ...
        self.sendOutput.emit(output)
    
    
    
    class MyQProcess(QWidget):     
      def __init__(self):    
       super(QWidget, self).__init__()
       layout = QVBoxLayout()
       self.edit = QTextEdit()
       self.thread = QThread()
    
       self.setupConnections()
    
       self.edit.setWindowTitle("QTextEdit Standard Output Redirection")
       layout.addWidget(self.edit)
       self.setLayout(layout)
       self.show()
    
      def setupConnections(self):
        self.worker = Worker()
        self.thread.finished.connect(self.worker.deleteLater)
        self.worker.sendOutput.connect(self.showOutput)
    
        self.worker.moveToThread(self.thread)
        self.thread.start()
    
      def __del__(self):
        if self.thread.isRunning():
          self.thread.quit()
          # Do some extra checking if thread has finished or not here if you want to
    
      #Define Slot Here 
      @pyqtSlot(QString)
      def showOutput(self, output):
        #self.edit.clear()
        self.edit.append(output)
    
    
    def main():  
        app = QApplication(sys.argv)
        w   = MyQProcess()
    
        return app.exec_()
    
    if __name__ == '__main__':
        main()
    

    Further clarification: As I've told @BrendanAbel in the comment section of his answer the issue with using slots with QThread is that the slots have the same thread affinity (=the thread they belong to) as the QThread instance itself, which is the same thread where the QThread was created from. The only - I repeat the only - thing that runs in a separate thread when it comes to a QThread is its event loop represented by QThread.run(). If you look on the Internet you will find out that this way of doing things is discouraged (unless you really, really know that you have to subclass QThread) because since the early versions of Qt 4 run() was abstract and you had to subclass QThread in order to use a QThread. Later the abstract run() got a concrete implementation hence the need of subclassing QThread was removed. About thread-safety and signals what @BrendanAbel wrote is only partially true. It comes down to the connection type (default is AutoConnection). If you manually specify the connection type you may actually render the signals thread-unsafe. Read more about this in the Qt documentation.