【问题标题】:Show realtime output of subprocess in a QDialog在 QDialog 中显示子进程的实时输出
【发布时间】:2019-08-08 20:48:42
【问题描述】:

我有一个脚本可以激活虚拟环境并在其中运行pip 命令。为此,我首先使用所需的命令创建一个bash 脚本,并将最终命令(最终运行脚本)传递给run_script(),它会逐行生成输出。子进程可以正常工作,并且可以将输出打印到控制台。

现在,我想要实现的是显示捕获的run_script() 的实时输出(逐行显示)(显示pip install ... 的安装进度)以及@ 中的QProgressBar 987654327@.

到目前为止,我尝试在ProgBarDialog 类中设置self.statusLabel 的文本,但这并没有按预期工作。我想我可以创建一个类似的循环

for line in output:
    self.statusLabel.setText(line)

并依次显示进程输出的每一行。但是我不知道如何准确地从输出中捕获每一行,因为输出是一个大字符串,因此,for line in output 当然会捕获字符而不是行。

如何操作输出以正确格式化输出,以便能够在 QDialog 内的小部件(例如 QLabel 或类似的东西)中显示它?

(可能是我的编码方式愚蠢或低效,因此欢迎提出任何建议)


最小可重现示例:

注意:需要testfile.py旁边的虚拟环境才能重现。

testfile.py

from subprocess import Popen, PIPE
import sys
import os

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QDialog, QVBoxLayout,
                             QHBoxLayout, QLabel, QProgressBar)


def has_bash():
    """
    Test if bash is available. If present the string `/bin/bash` is returned,
    an empty string otherwise.
    """
    res = Popen(
        ["which", "bash"], stdout=PIPE, stderr=PIPE, text="utf-8"
    )
    out, _ = res.communicate()
    shell = out.strip()
    return shell

def run_script(command):
    """
    Run the script and catch output of the subprocess line by line.
    The `command` argument is set in `run_pip()`.
    """
    process = Popen(command, stdout=PIPE, text="utf-8")

    while True:
        output = process.stdout.readline()
        if output == "" and process.poll() is not None:
            break
        if output:
            # TODO: show output in dialog together with a progressbar
            print(f"[PIP]: {output.strip()}")
    rc = process.poll()
    return rc

def run_pip(cmd, opt, package, venv_dir, venv_name):
    """
    Activate the virtual environment and run pip commands.
    """
    current_dir = os.path.dirname(os.path.realpath(__file__))
    script = os.path.join(current_dir, "run.sh")

    if has_bash():
        # create run script
        with open(script, "w") as f:
            f.write(
                "#!/bin/bash\n"
                f"source {venv_dir}/{venv_name}/bin/activate\n"
                f"pip {cmd}{opt}{package}\n"
                "deactivate\n"
            )
        # make it executable
        os.system(f"chmod +x {script}")
        # run script
        command = ["/bin/bash", script]
        run_script(command)


class ProgBarDialog(QDialog):
    """
    Dialog showing output and a progress bar during the installation process.
    """
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.setGeometry(675, 365, 325, 80)
        self.setFixedSize(350, 85)
        self.setWindowFlag(Qt.WindowCloseButtonHint, False)
        self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)

        h_Layout = QHBoxLayout(self)
        v_Layout = QVBoxLayout()
        h_Layout.setContentsMargins(0, 15, 0, 0)

        self.statusLabel = QLabel(self)
        self.placeHolder = QLabel(self)

        self.progressBar = QProgressBar(self)
        self.progressBar.setFixedSize(325, 23)
        self.progressBar.setRange(0, 0)

        v_Layout.addWidget(self.statusLabel)
        v_Layout.addWidget(self.progressBar)
        v_Layout.addWidget(self.placeHolder)

        h_Layout.addLayout(v_Layout)
        self.setLayout(h_Layout)



if __name__ == "__main__":

    cmd = ["install "]
    opt = ["--upgrade "]
    package = "pylint"  # this could be any package
    current_dir = os.path.dirname(os.path.realpath(__file__))
    venv_name = "testenv"  # a virtual env beside this test file

    run_pip(cmd[0], opt[0], package, current_dir, venv_name)

    #]=======================================================================[#

    app = QApplication(sys.argv)
    progBar = ProgBarDialog()
    progBar.show()
    sys.exit(app.exec_())

【问题讨论】:

    标签: python python-3.x pyqt subprocess pyqt5


    【解决方案1】:

    在这种情况下,最好使用 QProcess,因为它不会阻塞事件循环,并在有新输出时通过信号通知您:

    import os
    
    from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QProcess, Qt
    from PyQt5.QtGui import QFontMetrics
    from PyQt5.QtWidgets import QApplication, QDialog, QHBoxLayout, QLabel, QProgressBar, QVBoxLayout
    
    
    def has_bash():
        process = QProcess()
        process.start("which bash")
        process.waitForStarted()
        process.waitForFinished()
        if process.exitStatus() == QProcess.NormalExit:
            return bool(process.readAll())
        return False
    
    
    class PipManager(QObject):
        started = pyqtSignal()
        finished = pyqtSignal()
        textChanged = pyqtSignal(str)
    
        def __init__(self, venv_dir, venv_name, parent=None):
            super().__init__(parent)
    
            self._venv_dir = venv_dir
            self._venv_name = venv_name
    
            self._process = QProcess(self)
            self._process.readyReadStandardError.connect(self.onReadyReadStandardError)
            self._process.readyReadStandardOutput.connect(self.onReadyReadStandardOutput)
            self._process.stateChanged.connect(self.onStateChanged)
            self._process.started.connect(self.started)
            self._process.finished.connect(self.finished)
            self._process.finished.connect(self.onFinished)
            self._process.setWorkingDirectory(venv_dir)
    
        def run_command(self, command="", options=None):
            if has_bash():
                if options is None:
                    options = []
                script = f"""source {self._venv_name}/bin/activate; pip {command} {" ".join(options)}; deactivate;"""
                self._process.start("bash", ["-c", script])
    
        @pyqtSlot(QProcess.ProcessState)
        def onStateChanged(self, state):
            if state == QProcess.NotRunning:
                print("not running")
            elif state == QProcess.Starting:
                print("starting")
            elif state == QProcess.Running:
                print("running")
    
        @pyqtSlot(int, QProcess.ExitStatus)
        def onFinished(self, exitCode, exitStatus):
            print(exitCode, exitStatus)
    
        @pyqtSlot()
        def onReadyReadStandardError(self):
            message = self._process.readAllStandardError().data().decode().strip()
            print("error:", message)
            self.finished.emit()
            self._process.kill()
            """self.textChanged.emit(message)"""
    
        @pyqtSlot()
        def onReadyReadStandardOutput(self):
            message = self._process.readAllStandardOutput().data().decode().strip()
            self.textChanged.emit(message)
    
    
    class ProgBarDialog(QDialog):
        """
        Dialog showing output and a progress bar during the installation process.
        """
    
        def __init__(self):
            super().__init__()
            self.initUI()
    
        def initUI(self):
            self.setFixedWidth(400)
            self.setWindowFlag(Qt.WindowCloseButtonHint, False)
            self.setWindowFlag(Qt.WindowMinimizeButtonHint, False)
    
            self.statusLabel = QLabel()
            self.placeHolder = QLabel()
    
            self.progressBar = QProgressBar()
            self.progressBar.setFixedHeight(23)
            self.progressBar.setRange(0, 0)
    
            v_Layout = QVBoxLayout(self)
            v_Layout.addWidget(self.statusLabel)
            v_Layout.addWidget(self.progressBar)
            v_Layout.addWidget(self.placeHolder)
    
        @pyqtSlot(str)
        def update_status(self, status):
            metrix = QFontMetrics(self.statusLabel.font())
            clippedText = metrix.elidedText(status, Qt.ElideRight, self.statusLabel.width())
            self.statusLabel.setText(clippedText)
    
    
    if __name__ == "__main__":
        import sys
    
        app = QApplication(sys.argv)
    
        progBar = ProgBarDialog()
    
        current_dir = os.path.dirname(os.path.realpath(__file__))
        venv_name = "testenv"
    
        manager = PipManager(current_dir, venv_name)
        manager.textChanged.connect(progBar.update_status)
        manager.started.connect(progBar.show)
        manager.finished.connect(progBar.close)
    
        manager.run_command("install", ["--upgrade", "pylint"])
    
        sys.exit(app.exec_())
    

    【讨论】:

      猜你喜欢
      • 2013-02-28
      • 2018-10-02
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-09-20
      • 2013-08-27
      相关资源
      最近更新 更多