Capturing Output from a Process

Problem: You want to run a process that prints lots of information to the console and display the output in a text editor or browser, but the result is a GUI that freezes until the process is finished.

Solution (one of many possible): Create a QProcess object, connect its signals to some slots in your class, pass it the required arguments and start it. Data on the process's stdout and stderr is delivered to your slots.

An Example

The following is a small example that reads text from a QLineEdit and executes it in a separate process, collecting the output and displaying it in a QTextBrowser. The process can be started and stopped by means of two QPushButtons.

As well as the sys module, we'll use a collection of classes from the qt module:

   1 import sys
   2 from qt import qApp, QApplication, QGridLayout, QLineEdit, QProcess, \
               QPushButton, QString, QStringList, QTextBrowser, QTimer, \
               QWidget, SIGNAL, SLOT

We'll use a widget to contain the editor, line edit and buttons. Using a class like this also means that we can keep everything self-contained.

   1 class Window(QWidget):

In the __init__ method, we call the base class's __init__ method as usual and create the user interface elements:

   1     def __init__(self):
   2 
   3         QWidget.__init__(self)
   4 
   5         self.textBrowser = QTextBrowser(self)
   6         self.textBrowser.setTextFormat(QTextBrowser.LogText)
   7         self.lineEdit = QLineEdit(self)
   8         self.startButton = QPushButton(self.tr("Start"), self)
   9         self.stopButton = QPushButton(self.tr("Stop"), self)
  10         self.stopButton.setEnabled(False)

The Start button is enabled by default, but we want to disable the Stop button until a process is running.

We continue by connecting signals in the line edit and buttons to slots (ordinary Python methods, shown below) that perform the actions of starting and stopping the process. We want to start running the process whenever the user presses Return in the line edit, or presses the Start button. When this occurs, the startCommand() slot will be called.

   1         self.connect(self.lineEdit, SIGNAL("returnPressed()"), self.startCommand)
   2         self.connect(self.startButton, SIGNAL("clicked()"), self.startCommand)
   3         self.connect(self.stopButton, SIGNAL("clicked()"), self.stopCommand)

It is safe to connect the Stop button's clicked() signal to a slot now because the button is initially disabled. The user can't press it and activate the signal until it is enabled. When it is activated, the stopCommand() slot will be called.

We put all the user interface elements into a grid layout:

   1         layout = QGridLayout(self, 2, 3)
   2         layout.setSpacing(8)
   3         layout.addMultiCellWidget(self.textBrowser, 0, 0, 0, 2)
   4         layout.addWidget(self.lineEdit, 1, 0)
   5         layout.addWidget(self.startButton, 1, 1)
   6         layout.addWidget(self.stopButton, 1, 2)

Although we could create a new QProcess every time we want to run a process, it's easier to create one here, set up signals and slots connections, and re-use it. We're only going to run one process at a time, so this approach should work well.

   1         self.process = QProcess()
   2         self.connect(self.process, SIGNAL("readyReadStdout()"), self.readOutput)
   3         self.connect(self.process, SIGNAL("readyReadStderr()"), self.readErrors)
   4         self.connect(self.process, SIGNAL("processExited()"), self.resetButtons)

The readyReadStdout() and readyReadStderr() signals are connected to slots that will handle the data. The processExited() signal is connected to a slot that just manages the user interface; it could be extended to do other things.

The startCommand() slot is responsible for taking the input text from the line edit, preparing the user interface to accept output, and starting the process with the correct arguments.

   1     def startCommand(self):
   2         self.process.setArguments(QStringList.split(" ", self.lineEdit.text()))
   3         self.process.closeStdin()

To star with, the text from the line edit is split up into pieces and passed as arguments to the QProcess object we defined in the __init__ method. The arguments include the name of the external program we want to run.

The Start button is disabled to prevent the user from trying to start a new process while the current one is running, and the Stop button is enabled, so that the user is able to stop the current process.

   1         self.startButton.setEnabled(False)
   2         self.stopButton.setEnabled(True)
   3         self.textBrowser.clear()

The text browser's contents are also cleared so that new text can be inserted into it.

Now, we start the process, checking for failure in case the user's input was invalid in some way. If this happens, we write some text to the browser and reset the user interface, ready for the next set of user input:

   1         if not self.process.start():
   2             self.textBrowser.setText(
   3                 QString("*** Failed to run %1 ***").arg(self.lineEdit.text())
   4                 )
   5             self.resetButtons()
   6             return

The stopCommand() slot simply resets the user interface and tries to stop the process, waiting for 5 seconds and killing it if necessary.

   1     def stopCommand(self):
   2         self.resetButtons()
   3         self.process.tryTerminate()
   4         QTimer.singleShot(5000, self.process, SLOT("kill()"))

The readOutput() and readErrors() slots are called whenever the QProcess object emits the readyReadStdout() and readyReadStderr() signals. These simply take call methods on the QProcess object to collect the output or errors from the process, and append this text to the contents of the text browser.

   1     def readOutput(self):
   2 
   3         self.textBrowser.append(QString(self.process.readStdout()))
   4 
   5     def readErrors(self):
   6 
   7         self.textBrowser.append("error: " + QString(self.process.readLineStderr()))

The resetButtons() method just puts the buttons back in their initial state, ready for the user to start a new process:

   1     def resetButtons(self):
   2         self.startButton.setEnabled(True)
   3         self.stopButton.setEnabled(False)

For completeness, we include code to start a new application, create and show the window, and start Qt's event loop:

   1 if __name__ == "__main__":
   2 
   3     app = QApplication(sys.argv)
   4     window = Window()
   5     app.setMainWidget(window)
   6     window.show()
   7 
   8     sys.exit(app.exec_loop())

Comments

It seems like a lot of extra work to use signals and slots rather than a simple synchronous loop that collects output and calls qApp.processEvents(). However, because we return immediately after starting the process, and use the signals and slots mechanism to collect output, the application's event loop is run for us and the user interface is updated, even if the process produces no output for a long time.

Feel free to add more comments below here.

Capturing Output from a Process (last edited 2007-10-08 20:56:45 by DavidBoddie)