如何捕获长时间运行的程序的输出并将其呈现在Python中的GUI中?

时间:2017-01-18 20:35:13

标签: python tkinter pyqt stdout stderr

This is what I need to embedded into my GUI

我会尽可能地保持清醒。

我有一个非常简单的测试脚本来控制电源,该脚本测量来自安捷伦电源+被测单元的一些电流,然后,脚本打印这些读数就像这样简单:

PS.write(b"MEAS:CURR? \n")
time.sleep(2)
response = PS.read(1000)
time.sleep(3)
print(response)
(float(response)*1)
E3632A=(float(response)*1)
print (E3632A)

当脚本超出"打印命令" (打印(E3632A),所有信息都显示在" py.exe" DOS窗口(C:\ Windows \ py.exe)中。这是我的问题

如何将其嵌入到简单的GUI中?我希望我的GUI显示py.exe正在显示的数据。那么简单......我已经通过互联网阅读了所有帖子,没有一个真正的解决方案。

1 个答案:

答案 0 :(得分:2)

假设您正在调用的进程长时间运行但并未一次性生成所有输出,则表示您无法使用subprocess.Popen.communicate(),因为它旨在读取所有输出都到文件末尾。

您必须使用其他标准技术从管道中读取。

由于您希望将其与GUI集成并且流程长时间运行,您需要协调读取其输出与GUI的主循环。这有点复杂化了。

Tkinter的

让我们首先假设您要使用 TkInter ,就像您的一个示例中一样。这给我们带来了几个问题:

  • 没有将TkInter与选择模块集成。
  • 到目前为止,甚至还没有将TkInter与 asyncio 进行规范整合(另见https://bugs.python.org/issue27546)。
  • 通常建议使用root.update()将自定义主循环绑定在一起,让我们通过线程解决应该基于事件的方法。
  • TkInter event_generate()缺少Tk发送用户数据和事件的能力,因此我们无法使用TkInter事件将收到的输出从一个线程传递到另一个线程

因此,我们将使用线程解决它(即使我不喜欢),其中主线程控制Tk GUI并且辅助线程读取进程的输出,并且在TkInter中缺少传递数据的本地方式,我们使用线程安全的队列

#!/usr/bin/env python3

from subprocess import Popen, PIPE, STDOUT, TimeoutExpired
from threading import Thread, Event
from queue import Queue, Empty
from tkinter import Tk, Text, END


class ProcessOutputReader(Thread):

    def __init__(self, queue, cmd, params=(),
                 group=None, name=None, daemon=True):
        super().__init__(group=group, name=name, daemon=daemon)
        self._stop_request = Event()
        self.queue = queue
        self.process = Popen((cmd,) + tuple(params),
                             stdout=PIPE,
                             stderr=STDOUT,
                             universal_newlines=True)

    def run(self):
        for line in self.process.stdout:
            if self._stop_request.is_set():
                # if stopping was requested, terminate the process and bail out
                self.process.terminate()
                break

            self.queue.put(line)  # enqueue the line for further processing

        try:
            # give process a chance to exit gracefully
            self.process.wait(timeout=3)
        except TimeoutExpired:
            # otherwise try to terminate it forcefully
            self.process.kill()

    def stop(self):
        # request the thread to exit gracefully during its next loop iteration
        self._stop_request.set()

        # empty the queue, so the thread will be woken up
        # if it is blocking on a full queue
        while True:
            try:
                self.queue.get(block=False)
            except Empty:
                break

            self.queue.task_done()  # acknowledge line has been processed


class MyConsole(Text):

    def __init__(self, parent, queue, update_interval=50, process_lines=500):
        super().__init__(parent)
        self.queue = queue
        self.update_interval = update_interval
        self.process_lines = process_lines

        self.after(self.update_interval, self.fetch_lines)

    def fetch_lines(self):
        something_inserted = False

        for _ in range(self.process_lines):
            try:
                line = self.queue.get(block=False)
            except Empty:
                break

            self.insert(END, line)
            self.queue.task_done()  # acknowledge line has been processed

            # ensure scrolling the view is at most done once per interval
            something_inserted = True

        if something_inserted:
            self.see(END)

        self.after(self.update_interval, self.fetch_lines)


# create the root widget
root = Tk()

# create a queue for sending the lines from the process output reader thread
# to the TkInter main thread
line_queue = Queue(maxsize=1000)

# create a process output reader
reader = ProcessOutputReader(line_queue, 'python3', params=['-u', 'test.py'])

# create a console
console = MyConsole(root, line_queue)

reader.start()   # start the process
console.pack()   # make the console visible
root.mainloop()  # run the TkInter main loop

reader.stop()
reader.join(timeout=5)  # give thread a chance to exit gracefully

if reader.is_alive():
    raise RuntimeError("process output reader failed to stop")

由于前面提到的警告,TkInter代码在较大的一侧有点结束。

的PyQt

使用 PyQt ,我们可以大大改善我们的情况,因为该框架已经采用本机方式与子进程集成,其形式为 QProcess

这意味着我们可以取消线程并使用Qt的原生信号 Slot 机制。

#!/usr/bin/env python3

import sys

from PyQt5.QtCore import pyqtSignal, pyqtSlot, QProcess, QTextCodec
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import QApplication, QPlainTextEdit


class ProcessOutputReader(QProcess):
    produce_output = pyqtSignal(str)

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        # merge stderr channel into stdout channel
        self.setProcessChannelMode(QProcess.MergedChannels)

        # prepare decoding process' output to Unicode
        codec = QTextCodec.codecForLocale()
        self._decoder_stdout = codec.makeDecoder()
        # only necessary when stderr channel isn't merged into stdout:
        # self._decoder_stderr = codec.makeDecoder()

        self.readyReadStandardOutput.connect(self._ready_read_standard_output)
        # only necessary when stderr channel isn't merged into stdout:
        # self.readyReadStandardError.connect(self._ready_read_standard_error)

    @pyqtSlot()
    def _ready_read_standard_output(self):
        raw_bytes = self.readAllStandardOutput()
        text = self._decoder_stdout.toUnicode(raw_bytes)
        self.produce_output.emit(text)

    # only necessary when stderr channel isn't merged into stdout:
    # @pyqtSlot()
    # def _ready_read_standard_error(self):
    #     raw_bytes = self.readAllStandardError()
    #     text = self._decoder_stderr.toUnicode(raw_bytes)
    #     self.produce_output.emit(text)


class MyConsole(QPlainTextEdit):

    def __init__(self, parent=None):
        super().__init__(parent=parent)

        self.setReadOnly(True)
        self.setMaximumBlockCount(10000)  # limit console to 10000 lines

        self._cursor_output = self.textCursor()

    @pyqtSlot(str)
    def append_output(self, text):
        self._cursor_output.insertText(text)
        self.scroll_to_last_line()

    def scroll_to_last_line(self):
        cursor = self.textCursor()
        cursor.movePosition(QTextCursor.End)
        cursor.movePosition(QTextCursor.Up if cursor.atBlockStart() else
                            QTextCursor.StartOfLine)
        self.setTextCursor(cursor)


# create the application instance
app = QApplication(sys.argv)

# create a process output reader
reader = ProcessOutputReader()

# create a console and connect the process output reader to it
console = MyConsole()
reader.produce_output.connect(console.append_output)

reader.start('python3', ['-u', 'test.py'])  # start the process
console.show()                              # make the console visible
app.exec_()                                 # run the PyQt main loop

我们最终得到了一个源自Qt类的小样板,但总体上更清洁。

一般注意事项

还要确保您调用的进程不会缓冲多个输出行,否则它看起来仍然会像控制台卡住一样。

特别是如果被调用者是python程序,你可以确保它使用print(..., flush=True)或者用python -u callee.py调用它来强制执行无缓冲的输出。