运行 使用 PyQt5 命令并获取标准输出和标准错误

Run command with PyQt5 and getting the stdout and stderr

我想 运行 使用 PyQt5 命令。我想按时间顺序实时获取 stdout 和 stderr。

我分成了UIclass和工人class。有几个 UI class,但为了简单起见,我只指定了一个。

我试过解决这个问题,但我做不到。我无法在工作线程和记录器函数之间建立连接。

test_ui.py

import sys
import subprocess
from PyQt5.QtWidgets import QApplication, QWidget, QHBoxLayout, QVBoxLayout
from PyQt5.QtWidgets import QPushButton, QTextEdit
from worker import Worker


class TestUI(QWidget):
    def __init__(self):
        super().__init__()
        self.worker = Worker()
        self.btn1 = QPushButton("Button1")
        self.btn2 = QPushButton("Button2")
        self.btn3 = QPushButton("Button3")
        self.result = QTextEdit()
        self.init_ui()

    def init_ui(self):
        self.btn1.clicked.connect(self.press_btn1)
        self.btn2.clicked.connect(self.press_btn2)
        self.btn3.clicked.connect(self.press_btn3)

        hlayout1 = QHBoxLayout()
        hlayout1.addWidget(self.btn1)
        hlayout1.addWidget(self.btn2)
        hlayout1.addWidget(self.btn3)

        hlayout2 = QHBoxLayout()
        hlayout2.addWidget(self.result)

        vlayout = QVBoxLayout()
        vlayout.addLayout(hlayout1)
        vlayout.addLayout(hlayout2)

        self.setLayout(vlayout)
        self.show()

    def press_btn1(self):
        command1 = "dir"
        path = "./"
        self.worker.run_command(command1, path)
        self.worker.outSignal.connect(self.logging)

    def press_btn2(self):
        command2 = "cd"
        path = "./"
        self.worker.run_command(command2, path)
        self.worker.outSignal.connect(self.logging)

    def press_btn3(self):
        command3 = "whoami"
        path = "./"
        self.worker.run_command(command3, path)
        self.worker.outSignal.connect(self.logging)

    def logging(self, str):
        self.result.append(str.strip())


if __name__ == "__main__":
    APP = QApplication(sys.argv)
    ex = TestUI()
    sys.exit(APP.exec_())

worker.py


from PyQt5.QtCore import QProcess, pyqtSignal


class Worker:
    outSignal = pyqtSignal(str)
    errSignal = pyqtSignal(str)

    def __init__(self):
        self.proc = QProcess()

    def run_command(self, cmd, path):
        self.proc.setWorkingDirectory(path)
        self.proc.setProcessChannelMode(QProcess.MergedChannels)
        self.proc.readyReadStandardOutput.connect(self.onReadyStandardOutput)
        self.proc.finished.connect(self.proc.deleteLater)
        self.proc.start(cmd)

    def onReadyStandardOutput(self):
        result = self.proc.readAllStandardOutput().data().decode()
        self.outSignal.emit(result)

    def onReadyStandardError(self):
        result = self.proc.readAllStandardError().data().decode()
        self.errSignal.emit(result)

更新:

应用并进行以下修改仍然使代码失败:

@pyqtSlot()
def press_btn1(self):
    command1 = "dir"
    path = "./"
    self.worker.run_command(command1, path)

@pyqtSlot()
def press_btn2(self):
    command2 = "cd"
    path = "./"
    self.worker.run_command(command2, path)

@pyqtSlot()
def press_btn3(self):
    command3 = "test.bat"
    path = "./"
    self.worker.run_command(command3, path)

@pyqtSlot(str)
def logging(self, msg):
    msg = msg.strip()
    if msg != "":
        self.result.append(msg)

test.bat

@echo off

echo "Output 1"
timeout /t 1
1>&2 echo "Error 1"
timeout /t 1
echo "Output 2"
timeout /t 1
1>&2 echo "Error 2"

批处理文件问题

这是我通过命令提示符运行得到的结果。

实时每秒输出一行

"Output 1"

Waiting for 0 seconds, press a key to continue ...
"Error 1"

Waiting for 0 seconds, press a key to continue ...
"Output 2"

Waiting for 0 seconds, press a key to continue ...
"Error 2"

这是申请的结果。

3秒后输出整行。而且时间顺序不对

"Output 1"

Waiting for 1 seconds, press a key to continue ...0

Waiting for 1 seconds, press a key to continue ...0
"Output 2"

Waiting for 1 seconds, press a key to continue ...0
"Error 1"
"Error 2"

我不是很确定,但是你可以尝试让Worker继承自QObject或QWidget。我相当确定来自用户的信号需要 class 才能工作。

您有以下错误:

  • 信号只在QObject中起作用,所以Worker需要继承QObject。

  • 建议 QProcess 不要成为 class 的成员,因为我们说任务 1 正在执行并且没有完成就尝试执行任务 2,这样任务 1 将replaced 这不是你想要的,相反 QProcess 可以作为 Worker 的子代来完成,这样你的生命周期就不会局限于创建它的方法。

  • 如果你想分别监视 stderr 和 stdio 输出那么你不应该喜欢 processChannelMode 到 QProcess::MergedChannels 因为这将加入两个输出,另一方面如果上面被消除然后您必须使用 readyReadStandardError 信号来了解标准错误何时被修改。

  • 由于QProcess不是class的成员,所以很难在onReadyStandardOutput和onReadyStandardError中获取到QProcess,为此必须使用sender()方法,其中有发出信号的物体。

  • 信号和插槽之间的连接应该只建立一次,在你的情况下,你在 press_btn1、press_btn2 和 press_btn3 中进行,所以你会得到3次相同的信息。

  • 不要使用str,因为它是内置函数。

综合以上,解决方案是:

worker.py

from PyQt5.QtCore import QObject, QProcess, pyqtSignal, pyqtSlot


class Worker(QObject):
    outSignal = pyqtSignal(str)
    errSignal = pyqtSignal(str)

    def run_command(self, cmd, path):
        proc = QProcess(self)
        proc.setWorkingDirectory(path)
        proc.readyReadStandardOutput.connect(self.onReadyStandardOutput)
        proc.readyReadStandardError.connect(self.onReadyStandardError)
        proc.finished.connect(proc.deleteLater)
        proc.start(cmd)

    @pyqtSlot()
    def onReadyStandardOutput(self):
        proc = self.sender()
        result = proc.readAllStandardOutput().data().decode()
        self.outSignal.emit(result)

    @pyqtSlot()
    def onReadyStandardError(self):
        proc = self.sender()
        result = proc.readAllStandardError().data().decode()
        self.errSignal.emit(result)

test_ui.py

import sys

from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QApplication, QGridLayout, QPushButton, QTextEdit, QWidget

from worker import Worker


class TestUI(QWidget):
    def __init__(self):
        super().__init__()
        self.worker = Worker()
        self.worker.outSignal.connect(self.logging)
        self.btn1 = QPushButton("Button1")
        self.btn2 = QPushButton("Button2")
        self.btn3 = QPushButton("Button3")
        self.result = QTextEdit()
        self.init_ui()

    def init_ui(self):
        self.btn1.clicked.connect(self.press_btn1)
        self.btn2.clicked.connect(self.press_btn2)
        self.btn3.clicked.connect(self.press_btn3)

        lay = QGridLayout(self)
        lay.addWidget(self.btn1, 0, 0)
        lay.addWidget(self.btn2, 0, 1)
        lay.addWidget(self.btn3, 0, 2)
        lay.addWidget(self.result, 1, 0, 1, 3)

    @pyqtSlot()
    def press_btn1(self):
        command1 = "dir"
        path = "./"
        self.worker.run_command(command1, path)

    @pyqtSlot()
    def press_btn2(self):
        command2 = "cd"
        path = "./"
        self.worker.run_command(command2, path)

    @pyqtSlot()
    def press_btn3(self):
        command3 = "whoami"
        path = "./"
        self.worker.run_command(command3, path)

    @pyqtSlot(str)
    def logging(self, string):
        self.result.append(string.strip())


if __name__ == "__main__":
    APP = QApplication(sys.argv)
    ex = TestUI()
    ex.show()
    sys.exit(APP.exec_())

更新:

QProcess 在执行控制台命令(例如 .bat)方面有限制,因此在这种情况下,您可以通过在另一个线程中执行它并通过信号发送信息来使用 subprocess.Popen:

worker.py

import subprocess
import threading

from PyQt5 import QtCore


class Worker(QtCore.QObject):
    outSignal = QtCore.pyqtSignal(str)

    def run_command(self, cmd, **kwargs):
        threading.Thread(
            target=self._execute_command, args=(cmd,), kwargs=kwargs, daemon=True
        ).start()

    def _execute_command(self, cmd, **kwargs):
        proc = subprocess.Popen(
            cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs
        )
        for line in proc.stdout:
            self.outSignal.emit(line.decode())

test_ui.py

import sys

from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QApplication, QGridLayout, QPushButton, QTextEdit, QWidget

from worker import Worker


class TestUI(QWidget):
    def __init__(self):
        super().__init__()
        self.worker = Worker()
        self.worker.outSignal.connect(self.logging)
        self.btn1 = QPushButton("Button1")
        self.btn2 = QPushButton("Button2")
        self.btn3 = QPushButton("Button3")
        self.result = QTextEdit()
        self.init_ui()

    def init_ui(self):
        self.btn1.clicked.connect(self.press_btn1)
        self.btn2.clicked.connect(self.press_btn2)
        self.btn3.clicked.connect(self.press_btn3)

        lay = QGridLayout(self)
        lay.addWidget(self.btn1, 0, 0)
        lay.addWidget(self.btn2, 0, 1)
        lay.addWidget(self.btn3, 0, 2)
        lay.addWidget(self.result, 1, 0, 1, 3)

    @pyqtSlot()
    def press_btn1(self):
        command1 = "dir"
        path = "./"
        self.worker.run_command(command1, cwd=path)

    @pyqtSlot()
    def press_btn2(self):
        command2 = "cd"
        path = "./"
        self.worker.run_command(command2, cwd=path, shell=True)

    @pyqtSlot()
    def press_btn3(self):
        command3 = "test.bat"
        path = "./"
        self.worker.run_command(command3, cwd=path, shell=True)

    @pyqtSlot(str)
    def logging(self, string):
        self.result.append(string.strip())


if __name__ == "__main__":
    APP = QApplication(sys.argv)
    ex = TestUI()
    ex.show()
    sys.exit(APP.exec_())