Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I embed a python interpreter frame in python using tkinter?

I want to add a control terminal widget to my pure python+tkinter application similar to the python interpreter provided in Blender. It should be running within the same context (process) so the user can add features and control the application that is currently running from the control widget. Ideally I'd like it to also "hijack" stdout and stderr of the current application so it will report any problems or debugging information within the running application.

This is what I have come up with so far. The only problems are that it isn't responding to commands, and the thread doesn't stop when the user closes the window.

import Tkinter as tk
import sys
import code
from threading import *

class Console(tk.Frame):
    def __init__(self,parent=None):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        sys.stdout = self
        sys.stderr = self
        self.createWidgets()
        self.consoleThread = ConsoleThread()
        self.after(100,self.consoleThread.start)

    def write(self,string):
        self.ttyText.insert('end', string)
        self.ttyText.see('end')

    def createWidgets(self):
        self.ttyText = tk.Text(self.parent, wrap='word')
        self.ttyText.grid(row=0,column=0,sticky=tk.N+tk.S+tk.E+tk.W)


class ConsoleThread(Thread):

    def __init__(self):
        Thread.__init__(self)

    def run(self):
        vars = globals().copy()
        vars.update(locals())
        shell = code.InteractiveConsole(vars)
        shell.interact()

if __name__ == '__main__':
    root = tk.Tk()
    root.config(background="red")
    main_window = Console(root)
    main_window.mainloop()
    try:
        if root.winfo_exists():
            root.destroy()
    except:
        pass
like image 942
Ralph Ritoch Avatar asked Feb 16 '14 13:02

Ralph Ritoch


People also ask

How do you do framing in Python?

Practical Data Science using PythonIt works like a container, which is responsible for arranging the position of other widgets. It uses rectangular areas in the screen to organize the layout and to provide padding of these widgets. A frame can also be used as a foundation class to implement complex widgets.

How do I open one GUI from another GUI in Python?

You can't "call" another GUI. If this other GUI creates its own root window and calls mainloop() , your only reasonable option is to spawn a new process. That's a simple solution that requires little work. The two GUIs will be completely independent of each other.


1 Answers

it isn't responding to commands

The reason it isn't responding to commands is because you haven't linked the Text widget (self.ttyText) into stdin. Currently when you type it adds text into the widget and nothing else. This linking can be done similarly to what you've already done with stdout and stderr.

When implementing this, you need to keep track of which part of the text in the widget is the text being entered by the user - this can be done using marks (as described here).

the thread doesn't stop when the user closes the window.

I don't think there is a "clean" way to solve this issue without a major code re-write, however a solution that seems to work well enough is it simply detect when the widget is destroyed and write the string "\n\nexit()" to the interpreter. This calls the exit function inside the interpreter, which causes the call to shell.interact to finish, which makes the thread finish.

So without further ado, here is the modified code:

import tkinter as tk
import sys
import code
from threading import Thread
import queue


class Console(tk.Frame):
    def __init__(self, parent, _locals, exit_callback):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.exit_callback = exit_callback
        self.destroyed = False

        self.real_std_in_out = (sys.stdin, sys.stdout, sys.stderr)

        sys.stdout = self
        sys.stderr = self
        sys.stdin = self

        self.stdin_buffer = queue.Queue()

        self.createWidgets()

        self.consoleThread = Thread(target=lambda: self.run_interactive_console(_locals))
        self.consoleThread.start()

    def run_interactive_console(self, _locals):
        try:
            code.interact(local=_locals)
        except SystemExit:
            if not self.destroyed:
                self.after(0, self.exit_callback)

    def destroy(self):
        self.stdin_buffer.put("\n\nexit()\n")
        self.destroyed = True
        sys.stdin, sys.stdout, sys.stderr = self.real_std_in_out
        super().destroy()

    def enter(self, event):
        input_line = self.ttyText.get("input_start", "end")
        self.ttyText.mark_set("input_start", "end-1c")
        self.ttyText.mark_gravity("input_start", "left")
        self.stdin_buffer.put(input_line)

    def write(self, string):
        self.ttyText.insert('end', string)
        self.ttyText.mark_set("input_start", "end-1c")
        self.ttyText.see('end')

    def createWidgets(self):
        self.ttyText = tk.Text(self.parent, wrap='word')
        self.ttyText.grid(row=0, column=0, sticky=tk.N + tk.S + tk.E + tk.W)
        self.ttyText.bind("<Return>", self.enter)
        self.ttyText.mark_set("input_start", "end-1c")
        self.ttyText.mark_gravity("input_start", "left")

    def flush(self):
        pass

    def readline(self):
        line = self.stdin_buffer.get()
        return line


if __name__ == '__main__':
    root = tk.Tk()
    root.config(background="red")
    main_window = Console(root, locals(), root.destroy)
    main_window.mainloop()

This code has few changes other than those that solve the problems stated in the question.

The advantage of this code over my previous answer is that it works inside a single process, so can be created at any point in the application, giving the programmer more control.

I have also written a more complete version of this which also prevents the user from editing text which shouldn't be editable (e.g. the output of a print statement) and has some basic coloring: https://gist.github.com/olisolomons/e90d53191d162d48ac534bf7c02a50cd

like image 106
Oli Avatar answered Oct 05 '22 08:10

Oli