从QDialog中启动QProcess,用作进度监视器

时间:2016-02-18 22:51:49

标签: python pyqt qthread qprocess

我有一个主要的pyqt程序需要使用参数运行外部程序。我想使用QDialog作为一种状态监视器,它将在执行时捕获外部程序的标准输出并在QDialog内的文本框中显示它们。我有以下状态监视器代码:

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

然后我将使用以下命令实例化它:

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

到目前为止,这还没有奏效。

问题1:只有在取消注释waitForFinished(-1)行后,外部程序才会运行。

问题2. QDialog框只能在闪光灯下打开,然后消失。

问题3.显然,没有显示正在运行的程序的突出显示。

最后,我汇总的代码吸引了许多人的想法和教训,但我看一下,似乎它只能打印出程序结束后的所有突出,但我希望它当程序在运行时将它们写出来时,它将逐行显示。

我的工具链:Python 64位版本2.7.5,我正在Windows 7上开发

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

感谢您的帮助。

2 个答案:

答案 0 :(得分:4)

以下是您可以执行此操作的示例(我使用QWidget但您也可以使用QDialog或其他)。我没有使用单独的线程,因为UI不需要是交互式的。如果你想添加按钮等,那么你应该考虑选择运行Qt提供的QThread模型的好QObject

#!/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()

请注意,我正在使用的命令(df -h)运行一次(这是一个显示磁盘驱动器上磁盘使用情况的Linux命令)然后它就结束了。您也可以使用可以无限期运行的Execute.exe替换它。我已经使用htop(基于终端的高级任务管理器)对其进行了测试,除非用户想要它或系统停止(崩溃,关闭等),否则它一旦启动就不会停止。

请注意,您必须确保以干净的方式停止外部进程。这可以在__del__(析构函数)内部或在给定小部件生命周期结束时调用的另一个函数中完成。我所做的基本上是向外部流程发送SIGTERMterminate),一旦经过一段时间但流程仍在运行,我将情况提升为SIGKILLkill)。

代码显然需要更多的工作,但它应该足以让你知道事情是如何工作的。

这是上面代码的相同版本,但有一个额外的线程。请注意,我将外部进程的输出重定向到我的worker中的插槽。除非您想要处理该输出,否则您不必这样做。因此,您可以跳过此操作并将过程信号连接到接收它的小部件中的插槽并输出其内容。输出的处理将在单独的线程内再次完成,这样你就可以转移距离而不是冻结你的UI(如果你遵循

,就会发生这种情况)
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()

进一步澄清: 正如我在回答的评论部分中告诉@BrendanAbel一样,使用带QThread的插槽的问题是插槽具有与QThread实例相同的线程关联(=它们所属的线程)本身,与创建QThread的线程相同。唯一的 - 我重复唯一 - 在QThread时在单独的线程中运行的事情是由QThread.run()表示的事件循环。如果你在互联网上看,你会发现这种做事方式是不鼓励的(除非你真的,真的知道你必须继承QThread)因为Qt 4 run()的早期版本是抽象,您必须子类QThread才能使用QThread。后来,抽象run()得到了具体的实现,因此删除了子类化QThread的需要。关于线程安全性和@BrendanAbel所写的信号只是部分正确。它归结为连接类型(默认为AutoConnection)。如果手动指定连接类型,实际上可能会使信号线程不安全。请在Qt documentation中了解详情。

答案 1 :(得分:0)

您不能等待进程在同一个线程中更新GUI。 GUI仅在事件循环的每次迭代时更新。如果事件循环停留等待进程,则无法更新GUI。

解决方案是在一个单独的线程中监视进程,释放主线程以继续更新GUI。大多数GUI元素都不是线程安全的,因此您无法直接从监视线程将输出写入QTextEdit。但Signals是线程安全的,因此您可以使用一个信号将监视线程的输出发送回主线程,主线程中的QDialog可以处理并将输出打印到{{ 1}}

QTextEdit