如何使用tkinter在python中嵌入python解释器框架?

时间:2014-02-16 13:03:17

标签: python tkinter embed interpreter

我想在我的纯python + tkinter应用程序中添加一个控制终端小部件,类似于Blender中提供的python解释器。它应该在相同的上下文(进程)中运行,以便用户可以添加功能并控制当前从控件小部件运行的应用程序。理想情况下,我希望它还“劫持”当前应用程序的stdout和stderr,以便在运行的应用程序中报告任何问题或调试信息。

这是我到目前为止所提出的。唯一的问题是它没有响应命令,并且当用户关闭窗口时线程不会停止。

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

2 个答案:

答案 0 :(得分:5)

我有答案,万一有人还在乎! (我也改为python 3,因此import tkinter而不是import Tkinter

我通过使用单独的文件来运行InteractiveConsole,然后使主文件打开另一个文件(我称之为console.py并且位于相同目录),以编程方式将此子流程的stdout,stderr和stdin链接到tkinter Text小部件。

以下是控制台文件中的代码(如果这是正常运行,它就像一个普通的控制台):

# console.py
import code

if __name__ == '__main__':
    vars = globals().copy()
    vars.update(locals())
    shell = code.InteractiveConsole(vars)
    shell.interact() 

这是python解释器的代码,它在Text小部件中运行控制台:

# main.py
import tkinter as tk
import subprocess
import queue
import os
from threading import Thread

class Console(tk.Frame):
    def __init__(self,parent=None):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.createWidgets()

        # get the path to the console.py file assuming it is in the same folder
        consolePath = os.path.join(os.path.dirname(__file__),"console.py")
        # open the console.py file (replace the path to python with the correct one for your system)
        # e.g. it might be "C:\\Python35\\python"
        self.p = subprocess.Popen(["python3",consolePath],
                                  stdout=subprocess.PIPE,
                                  stdin=subprocess.PIPE,
                                  stderr=subprocess.PIPE)

        # make queues for keeping stdout and stderr whilst it is transferred between threads
        self.outQueue = queue.Queue()
        self.errQueue = queue.Queue()

        # keep track of where any line that is submitted starts
        self.line_start = 0

        # make the enter key call the self.enter function
        self.ttyText.bind("<Return>",self.enter)

        # a daemon to keep track of the threads so they can stop running
        self.alive = True
        # start the functions that get stdout and stderr in separate threads
        Thread(target=self.readFromProccessOut).start()
        Thread(target=self.readFromProccessErr).start()

        # start the write loop in the main thread
        self.writeLoop()

    def destroy(self):
        "This is the function that is automatically called when the widget is destroyed."
        self.alive=False
        # write exit() to the console in order to stop it running
        self.p.stdin.write("exit()\n".encode())
        self.p.stdin.flush()
        # call the destroy methods to properly destroy widgets
        self.ttyText.destroy()
        tk.Frame.destroy(self)
    def enter(self,e):
        "The <Return> key press handler"
        string = self.ttyText.get(1.0, tk.END)[self.line_start:]
        self.line_start+=len(string)
        self.p.stdin.write(string.encode())
        self.p.stdin.flush()

    def readFromProccessOut(self):
        "To be executed in a separate thread to make read non-blocking"
        while self.alive:
            data = self.p.stdout.raw.read(1024).decode()
            self.outQueue.put(data)

    def readFromProccessErr(self):
        "To be executed in a separate thread to make read non-blocking"
        while self.alive:
            data = self.p.stderr.raw.read(1024).decode()
            self.errQueue.put(data)

    def writeLoop(self):
        "Used to write data from stdout and stderr to the Text widget"
        # if there is anything to write from stdout or stderr, then write it
        if not self.errQueue.empty():
            self.write(self.errQueue.get())
        if not self.outQueue.empty():
            self.write(self.outQueue.get())

        # run this method again after 10ms
        if self.alive:
            self.after(10,self.writeLoop)

    def write(self,string):
        self.ttyText.insert(tk.END, string)
        self.ttyText.see(tk.END)
        self.line_start+=len(string)

    def createWidgets(self):
        self.ttyText = tk.Text(self, wrap=tk.WORD)
        self.ttyText.pack(fill=tk.BOTH,expand=True)


if __name__ == '__main__':
    root = tk.Tk()
    root.config(background="red")
    main_window = Console(root)
    main_window.pack(fill=tk.BOTH,expand=True)
    root.mainloop()

从stdout和stderr读取是在单独的线程中的原因是因为read方法是阻塞的,这导致程序冻结,直到console.py子进程提供更多输出,除非这些是在单独的线程中。由于tkinter不是线程安全的,因此需要writeLoop方法和队列来写入Text小部件。

这当然还有待解决的问题,例如,即使已经提交过文本窗口小部件上的任何代码都是可编辑的,但希望它可以回答您的问题。

编辑:我还整理了一些tkinter,使得控制台的行为更像标准小部件。

答案 1 :(得分:0)

  

它不响应命令

之所以不响应命令,是因为您尚未将Text小部件(self.ttyText)链接到stdin中。当前,当您键入时,它将文本添加到窗口小部件中,而没有其他内容。可以像使用stdoutstderr一样进行此链接。

实施此操作时,您需要跟踪小部件中的哪一部分是用户输入的文本-可以使用标记(as described here)。

  

当用户关闭窗口时,线程不会停止。

我不认为没有一种“干净的”方法可以解决这个问题,而无需重写主要的代码,但是一种似乎很好用的解决方案是,它只是检测小部件何时被破坏并写入字符串{ {1}}到口译员。这将调用解释器内部的"\n\nexit()"函数,从而导致对exit的调用完成,从而使线程完成。

因此,事不宜迟,下面是修改后的代码:

shell.interact

此代码除了解决问题中所述的问题外,几乎没有其他更改。

此代码相对于我先前的答案的优势在于它可以在单个进程中运行,因此可以在应用程序中的任何位置创建,从而赋予程序员更多的控制权。

我还写了一个更完整的版本,它还防止用户编辑不应编辑的文本(例如,打印语句的输出),并带有一些基本的颜色:https://gist.github.com/olisolomons/e90d53191d162d48ac534bf7c02a50cd