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

Posted

技术标签:

【中文标题】如何使用 tkinter 在 python 中嵌入 python 解释器框架?【英文标题】:How can I embed a python interpreter frame in python using tkinter? 【发布时间】:2014-03-15 17:01:14 【问题描述】:

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

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

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

【问题讨论】:

***.com/questions/21603038/…的可能副本 这个问题很相似,但这个问题是如何在 tkinter Frame 中制作交互式终端,并拦截 stdout 和 stderr,提到 stdin 是一个错字,我会修复。 IDLE 有类似的东西叫做Python Shell window,你可以阅读它的source code。 我现在已经解决了大部分问题。我必须实现自己的标准输入,解释器从中读取,还必须处理关键事件并在我也只读的文本小部件上输入事件。我仍然有失控线程的问题。当窗口关闭时,我将 quit() 推送给解释器,这有时会起作用。我也不知道如何在quit() 上关闭控制台窗口。该线程似乎也从未从 shell.interact() 返回,这可能就是为什么有些线程在我身上跑掉的原因。 这很有趣,谢谢!但是shell似乎没有响应任何命令..你也是这种情况吗? 【参考方案1】:

如果有人仍然关心,我有答案! (我也改成 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,使控制台的行为更像一个标准小部件。

【讨论】:

我测试过,效果很好!我的一个问题是,您是如何确定需要覆盖哪些功能的?比如readFromProcessOut,我看不出它是如何工作的,入口点在哪里。另外我想知道是否可以有单独的输入和输出小部件。 @Sertalp Bilal 唯一被覆盖的方法是 __init__destroy。所有其他方法都被调用、绑定到键或在我的代码中的新线程中启动。例如readFromProcessOut__init__ 方法底部附近的新线程中启动。由于我这样做的方式,完全有可能拥有单独的输入和输出小部件 - 这需要一些代码重组,但只要两个小部件都引用相同的 subproccess 对象,它就会工作(你可以为此提出一个新问题,我不介意回答)【参考方案2】:

它没有响应命令

它不响应命令的原因是您没有将Text 小部件 (self.ttyText) 链接到 stdin。当前,当您键入时,它会将文本添加到小部件中,而没有其他任何内容。此链接的完成方式与您对 stdoutstderr 所做的类似。

在实现这一点时,您需要跟踪小部件中文本的哪一部分是用户输入的文本 - 这可以使用标记 (as described here) 来完成。

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

我认为没有一种“干净”的方法可以在不重写主要代码的情况下解决这个问题,但是一个似乎工作得很好的解决方案是它只是检测小部件何时被销毁并写入字符串@ 987654328@ 给口译员。这会调用解释器内部的exit 函数,这会导致对shell.interact 的调用结束,从而使线程结束。

废话不多说,修改后的代码如下:

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

除了解决问题中提到的问题之外,此代码几乎没有变化。

与我之前的答案相比,这段代码的优势在于它在单个进程中工作,因此可以在应用程序的任何位置创建,从而为程序员提供更多控制权。

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

【讨论】:

以上是关于如何使用 tkinter 在 python 中嵌入 python 解释器框架?的主要内容,如果未能解决你的问题,请参考以下文章

我如何在 Python 中嵌入 Discord 中换行?

在C语言中如何嵌入python脚本

如何在 Python 中嵌入 Lua?

如何在 boost::python 嵌入式 python 代码中导入模块?

如何中断嵌入在 C++ 应用程序中的 python 解释器

如何在未安装 python 的系统上嵌入 python 代码来执行 C++ 代码