20

I have a Python program which performs a set of operations and prints the response on STDOUT. Now I am writing a GUI which will call that already existing code and I want to print the same contents in the GUI instead of STDOUT. I will be using the Text widget for this purpose. I do not want to modify my existing code which does the task (This code is used by some other programs as well).

Can someone please point me to how I can use this existing task definition and use its STDOUT result and insert it into a text widget? In the main GUI program I want to call this task definition and print its results to STDOUT. Is there a way to use this information?

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
sarbjit
  • 3,786
  • 9
  • 38
  • 60

5 Answers5

26

You can probably solve this by replacing sys.stdout with your own file-like object that writes to the text widget.

For example:

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()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • can you please throw some light on 'file-like' object. – sarbjit Sep 10 '12 at 13:03
  • Thanks a lot for your help. I have included this TextRedirector class in my GUI module and have configured to use stdout. It solved my purpose. – sarbjit Sep 10 '12 at 14:07
  • Just one query : How can i stop the redirection to stdout. I tried "sys.stdout = sys.__stdout__" after my function call is over. Then when i try print statement, output is not seen in console (nor in Text widget). – sarbjit Sep 11 '12 at 05:06
  • 3
    I preserved the original reference of stdout as "old_stdout =sys.stdout" before modifying its reference to Redirect class and later on restore it back to old_stdout once function call is over, then it worked fine (Got the output on console) – sarbjit Sep 11 '12 at 05:16
  • 1
    `contextlib.redirect_stdout()` works really well with this: `tr = TextRedirector(widget); with redirect_stdout(tr): ...` – z33k Mar 02 '22 at 10:49
  • @Bryan Oakley: Thank you very much for this answer. I added a `self.widget.update()` to the `write` method of the TextRedirector class so that the output is updated to a ScrolledText widget after every time the print function is called. – Ji Wei Dec 28 '22 at 14:23
16

In python, whenever you call print('examplestring'), you're indirectly calling 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()

Method 1: Print out on GUI

def redirector(inputStr):
    textbox.insert(INSERT, inputStr)

sys.stdout.write = redirector #whenever sys.stdout.write is called, redirector is called.

root.mainloop()

Infact we're calling print -(callsfor)-> sys.stdout.write -(callsfor)-> redirector

Method 2: Writing a decorator - print out on both CLI and 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()

What a decorator does is it actually assign the func sys.stdout.write to func inner

sys.stdout.write=inner

and func inner adds an extra line of code before calling back the actual sys.stdout.write

This is in a way updating the older func sys.stdout.write to have new feature. You will notice that I used a try-except such that if there's any error in printing to the textbox, I would at least retain the original func of sys.stdout.write to the CLI

Method 3: Bryan Oakley's example

...
    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")

What he did was that he assigned sys.stdout to Class TextRedirector with a Method .write(str)

so calling print('string') -calls for-> sys.stdout.write('string') -callsfor-> TextRedirector.write('string')

Yuukio
  • 239
  • 3
  • 4
0

You can call the CLI program using subprocess.Popen, grab the stdout it produces, and display it in the text widget.

Something along the lines of (untested):

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)

Note that this will block until the CLI program finishes executing, freezing your GUI. To get around the blocking, you can put this code in a threading.Thread and let the GUI update while waiting for the thread to finish.

pR0Ps
  • 2,752
  • 2
  • 23
  • 26
-1

In fact, I think this problem is not limited to tkinter. Any framework can be applied because it is actually redirecting sys.stdout.

I created a class (RedirectStdMsg) to do this.

tl;dr

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)

Test

test tk class

modified from @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")

test cases

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

this is test_tk_have_stop_btn(): enter image description here

Carson
  • 6,105
  • 2
  • 37
  • 45
-2

The function which normally prints to stdout should instead put the text into text widget.

Some programmer dude
  • 400,186
  • 35
  • 402
  • 621
  • Problem is the original function is coded in such a way that it performs some sequence of operations and prints the result (in that function print command is used for displaying the status). This function is shared across few other programs (All are command line based functions). So i was just thinking if there could be some way to address this problem in Main GUI program itself. – sarbjit Sep 10 '12 at 12:44