在 QDialog 中显示子进程的实时输出 [英] Show realtime output of subprocess in a QDialog

查看:80
本文介绍了在 QDialog 中显示子进程的实时输出的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a script that activates a virtual environment and runs pip commands inside it. For that I first create a bash script with the needed commands and pass the final command (which finally runs the script) to run_script() which produces an output line by line. The subprocess works fine as well as printing the output to a console.

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

Now, what I'm trying to achieve is to display the catched realtime output (that comes line by line) of run_script() (which shows the installation progress of pip install ...) together with a QProgressBar in a QDialog.

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

So far, I tried to set the text of self.statusLabel in ProgBarDialog class, but that doesn't work as expected. I thought I could create a loop similar to this

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

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

and show each line of the process output after the other. But I don't know how exactly to catch each line from the output since the output comes as a big string and because of that, of course for line in output catches the characters and not the lines.

我如何操作输出以正确的方式对其进行格式化,以便能够在 QDialog?

How do I have to manipulate the output to format it the correct way to be able to show it in a widget (for example a QLabel or something similar) inside a QDialog?

(可能是我编码的方式很愚蠢或效率低下,所以欢迎提出任何建议)

注意:复制需要testfile.py旁边的虚拟环境.

Note: A virtual environment beside testfile.py is needed to reproduce.

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

推荐答案

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

In this case it is better to use QProcess since it does not block the eventloop and notifies you through a signal when there is a new output:

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

这篇关于在 QDialog 中显示子进程的实时输出的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆