我有一个 Python 程序,它执行一组操作并在 STDOUT 上打印响应。现在我正在编写一个 GUI,它将调用已经存在的代码,并且我想在 GUI 中而不是 STDOUT 中打印相同的内容。为此,我将使用 Text 小部件。我不想修改执行该任务的现有代码(此代码也被其他一些程序使用)。
有人可以指出我如何使用这个现有的任务定义并使用它的 STDOUT 结果并将其插入到文本小部件中吗?在主 GUI 程序中,我想调用此任务定义并将其结果打印到 STDOUT。有没有办法使用这些信息?
您可以通过替换sys.stdout
为您自己的写入文本小部件的类似文件的对象来解决此问题。
例如:
import Tkinter as tk
import sys
class ExampleApp(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
toolbar = tk.Frame(self)
toolbar.pack(side="top", fill="x")
b1 = tk.Button(self, text="print to stdout", command=self.print_stdout)
b2 = tk.Button(self, text="print to stderr", command=self.print_stderr)
b1.pack(in_=toolbar, side="left")
b2.pack(in_=toolbar, side="left")
self.text = tk.Text(self, wrap="word")
self.text.pack(side="top", fill="both", expand=True)
self.text.tag_configure("stderr", foreground="#b22222")
sys.stdout = TextRedirector(self.text, "stdout")
sys.stderr = TextRedirector(self.text, "stderr")
def print_stdout(self):
'''Illustrate that using 'print' writes to stdout'''
print "this is stdout"
def print_stderr(self):
'''Illustrate that we can write directly to stderr'''
sys.stderr.write("this is stderr\n")
class TextRedirector(object):
def __init__(self, widget, tag="stdout"):
self.widget = widget
self.tag = tag
def write(self, str):
self.widget.configure(state="normal")
self.widget.insert("end", str, (self.tag,))
self.widget.configure(state="disabled")
app = ExampleApp()
app.mainloop()
在 python 中,每当你调用 print('examplestring') 时,你都是在间接调用 sys.stdout.write('examplestring') :
from tkinter import *
root=Tk()
textbox=Text(root)
textbox.pack()
button1=Button(root, text='output', command=lambda : print('printing to GUI'))
button1.pack()
方法一:在GUI上打印
def redirector(inputStr):
textbox.insert(INSERT, inputStr)
sys.stdout.write = redirector #whenever sys.stdout.write is called, redirector is called.
root.mainloop()
事实上,我们正在调用 print -(callsfor)-> sys.stdout.write -(callsfor)-> redirector
方法 2:编写装饰器 - 在 CLI 和 GUI 上打印
def decorator(func):
def inner(inputStr):
try:
textbox.insert(INSERT, inputStr)
return func(inputStr)
except:
return func(inputStr)
return inner
sys.stdout.write=decorator(sys.stdout.write)
#print=decorator(print) #you can actually write this but not recommended
root.mainloop()
装饰器所做的实际上是将 func sys.stdout.write 分配给 func inner
sys.stdout.write=inner
并且 func inner 在回调实际的 sys.stdout.write 之前添加了额外的代码行
这在某种程度上更新了旧的 func sys.stdout.write 以具有新功能。您会注意到我使用了 try-except ,这样如果在打印到文本框时出现任何错误,我至少会将 sys.stdout.write 的原始函数保留到 CLI
方法 3:Bryan Oakley 的例子
...
sys.stdout = TextRedirector(self.text, "stdout")
...
class TextRedirector(object):
def __init__(self, widget, tag="stdout"):
self.widget = widget
self.tag = tag
def write(self, str):
self.widget.configure(state="normal")
self.widget.insert("end", str, (self.tag,))
self.widget.configure(state="disabled")
他所做的是将 sys.stdout 分配给 Class TextRedirector 并使用 Method .write(str)
所以调用 print('string') -calls for-> sys.stdout.write('string') -callsfor-> TextRedirector.write('string')
您可以使用 调用 CLI 程序subprocess.Popen
,获取它生成的标准输出,并将其显示在文本小部件中。
类似于(未经测试)的东西:
import subprocess
with subprocess.Popen(your_CLI_program, stdout=subprocess.PIPE) as cli
line = cli.stdout.readline()
#process the output of your_CLI_program
print (line)
请注意,这将一直阻塞,直到 CLI 程序完成执行,冻结您的 GUI。要绕过阻塞,您可以将此代码放在 a 中threading.Thread
,并在等待线程完成时让 GUI 更新。
其实我觉得这个问题不限于tkinter
. 任何框架都可以应用,因为它实际上是在重定向sys.stdout
.
我创建了一个类 ( RedirectStdMsg
) 来执行此操作。
tl;博士
original = sys.stdout
sys.stdout = everything_you_like
...
sys.stdout = original # restore
import sys
from typing import TextIO
from typing import Callable
# import tkinter as tk
class RedirectStdMsg:
__slots__ = ('original', 'output_device',)
def __init__(self, sys_std: TextIO):
self.output_device = None
self.original = sys_std
def __call__(self, output_device=Callable[[str], None]):
self.output_device = output_device
return self
def __enter__(self):
if self.output_device is None:
raise AttributeError('output_device is empty')
self.start(self.output_device)
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_val:
self.write(str(exc_val))
self.stop()
def start(self, output_device):
self.output_device = output_device
std_name = self.original.name.translate(str.maketrans({'<': '', '>': ''}))
exec(f'sys.{std_name} = self') # just like: ``sys.stderr = self``
def stop(self):
std_name = self.original.name.translate(str.maketrans({'<': '', '>': ''}))
exec(f'sys.{std_name} = self.original')
self.output_device = None
def write(self, message: str):
""" When sys.{stderr, stdout ...}.write is called, it will redirected here"""
if self.output_device is None:
self.original.write(message)
self.original.flush()
return
self.output_device(message)
改编自@Bryan Oakley
class ExampleApp(tk.Tk):
def __init__(self, **options):
tk.Tk.__init__(self)
toolbar = tk.Frame(self)
toolbar.pack(side="top", fill="x")
b1 = tk.Button(self, text="print to stdout", command=self.print_stdout)
b2 = tk.Button(self, text="print to stderr", command=self.print_stderr)
b1.pack(in_=toolbar, side="left")
b2.pack(in_=toolbar, side="left")
self.text = tk.Text(self, wrap="word")
self.text.pack(side="top", fill="both", expand=True)
self.text.tag_configure("stderr", foreground="#b22222")
self.re_stdout = options.get('stdout')
self.re_stderr = options.get('stderr')
if self.re_stderr or self.re_stderr:
tk.Button(self, text='Start redirect', command=self.start_redirect).pack(in_=toolbar, side="left")
tk.Button(self, text='Stop redirect', command=self.stop_redirect).pack(in_=toolbar, side="left")
def start_redirect(self):
self.re_stdout.start(TextRedirector(self.text, "stdout").write) if self.re_stdout else ...
self.re_stderr.start(TextRedirector(self.text, "stderr").write) if self.re_stderr else ...
def stop_redirect(self):
self.re_stdout.stop() if self.re_stdout else ...
self.re_stderr.stop() if self.re_stderr else ...
@staticmethod
def print_stdout():
"""Illustrate that using 'print' writes to stdout"""
print("this is stdout")
@staticmethod
def print_stderr():
"""Illustrate that we can write directly to stderr"""
sys.stderr.write("this is stderr\n")
class TextRedirector(object):
def __init__(self, widget, tag="stdout"):
self.widget = widget
self.tag = tag
def write(self, msg):
self.widget.configure(state="normal")
self.widget.insert("end", msg, (self.tag,))
self.widget.configure(state="disabled")
def test_tk_without_stop_btn():
app = ExampleApp()
with RedirectStdMsg(sys.stdout)(TextRedirector(app.text, "stdout").write), \
RedirectStdMsg(sys.stderr)(TextRedirector(app.text, "stderr").write):
app.mainloop()
def test_tk_have_stop_btn():
director_out = RedirectStdMsg(sys.stdout)
director_err = RedirectStdMsg(sys.stderr)
app = ExampleApp(stdout=director_out, stderr=director_err)
app.mainloop()
def test_to_file():
# stdout test
with open('temp.stdout.log', 'w') as file_obj:
with RedirectStdMsg(sys.stdout)(file_obj.write):
print('stdout to file')
print('stdout to console')
# stderr test
with open('temp.stderr.log', 'w') as file_obj:
with RedirectStdMsg(sys.stderr)(file_obj.write):
sys.stderr.write('stderr to file')
sys.stderr.write('stderr to console')
# another way
cs_stdout = RedirectStdMsg(sys.stdout)
cs_stdout.start(open('temp.stdout.log', 'a').write)
print('stdout to file 2')
...
cs_stdout.stop()
print('stdout to console 2')
if __name__ == '__main__':
test_to_file()
test_tk_without_stop_btn()
test_tk_have_stop_btn()
通常打印到标准输出的函数应该将文本放入文本小部件中。