问题描述
我有一个脚本可以激活虚拟环境并在其中运行 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 中显示子进程的实时输出的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!