如何使 tkinter 文本框成为标准输入接收器?

How to make tkinter text box a stdin input receiver?

我制作了两个 tkinter 文本框,其中一个将您的 python 脚本作为输入,另一个显示脚本执行的结果但是当我使用 input() 命令时出现错误.下面给出的是标准输出重定向器的 class 以及在读取脚本后执行的执行函数,它工作正常。我没有包括 Texttkinter 等,因为我使用了所有适用于代码的通用方法,如 Text.get()Text.mark_set()Text.replace() 等,并且还有一些功能没有包括在这里。除了脚本和输出框之外,我还尝试将整个控制台嵌入到带有 InteractiveConsole 的文本框中,但是在接收输入或标准输入的情况下问题是相同的,但在这两种情况下都是 stdoutstderr 工作正常。

from code import InteractiveConsole, InteractiveInterpreter


class StdoutRedirector(object):
    def __init__(self, text_widget):
        self.text_space = text_widget

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


##class StdinRedirector(object):
##    def __init__(self, text_widget):
##        self.text_space = text_widget
##
##    def readline(self) -> str:
##        t = self.text_space.get(INSERT, f"{int(text.index(INSERT).split('.')[0])}.{int(text.index(INSERT).split('.')[1])}")
##        return t


def execute(event=None):
    save()
    code = text.get('1.0',END+'-1c')
    stdin = sys.stdin
    stdout = sys.stdout 
    stderr = sys.stderr

    output.delete('1.0',END)
##    def a():
##        sys.stdin = StdinRedirector(output)
##    output.bind('<Return>', lambda: a)
    
    sys.stdout = StdoutRedirector(output)
    sys.stderr = StdoutRedirector(output)
    
    interp = InteractiveInterpreter()
    interp.runcode(code)

    sys.stdout = stdout
    sys.stderr = stderr
##    sys.stdin = stdin

之后我尝试了重定向 stdin,这显然不起作用,应用程序挂起并且 window 即使在一次又一次尝试后也停止响应。 请帮助我...我不知道这是否不可能,但 PyCharm 和其他人内部有 I/O 流,所以也许控制台或执行 window 可以完全嵌入一个文本框。

好的,在网上、文档以及队列、idlelib 和子进程模块的代码中进行研究之后,我想出了使 tkinter 文本框与 python 控制台作为标准输入交互的最简单方法, stdout 和 stderr 接收器。这是代码:

import tkinter as tk
import subprocess
import queue
import os
from threading import Thread


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

        # create widgets
        self.ttytext = tk.Text(self, wrap=tk.WORD)
        self.ttytext.pack(fill=tk.BOTH, expand=True)
        self.ttytext.linenumbers.pack_forget()

        self.p = subprocess.Popen(["jupyter", "qtconsole"], stdout=subprocess.PIPE, stdin=subprocess.PIPE,
                                  stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NO_WINDOW)

        # 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

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

        # key bindings for events
        self.ttytext.bind("<Return>", self.enter)
        self.ttytext.bind('<BackSpace>', self.on_bkspace)
        self.ttytext.bind('<Delete>', self.on_delete)
        self.ttytext.bind('<<Copy>>', self.on_copy)
        self.ttytext.bind('<<Paste>>', self.on_paste)
        self.ttytext.bind('<Control-c>', self.on_copy)
        self.ttytext.bind('<Control-v>', self.on_paste)

    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, event):
        """The <Return> key press handler"""
        cur_ind = str(self.ttytext.index(tk.INSERT))
        if int(cur_ind.split('.')[0]) < int(self.ttytext.search(': ', tk.END, backwards=True).split('.')[0]):
            try:
                selected = self.ttytext.get('sel.first', 'sel.last')
                if len(selected) > 0:
                    self.ttytext.insert(tk.END, selected)
                    self.ttytext.mark_set(tk.INSERT, tk.END)
                    self.ttytext.see(tk.INSERT)
                    return 'break'
            except:
                selected = self.ttytext.get(
                    self.ttytext.search(': ', tk.INSERT, backwards=True), tk.INSERT)
                self.ttytext.insert(tk.END, selected.strip(': '))
                self.ttytext.mark_set(tk.INSERT, tk.END)
                self.ttytext.see(tk.INSERT)
            return 'break'
        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 on_bkspace(self, event):
        pass

    def on_delete(self, event):
        pass

    def on_key(self, event):
        """The typing control (<KeyRelease>) handler"""
        cur_ind = str(self.ttytext.index(tk.INSERT))
        try:
            if int(cur_ind.split('.')[0]) < int(self.ttytext.search(r'In [0-9]?', tk.END, backwards=True).split('.')[0]):
                return 'break'
        except:
            return

    def on_copy(self, event):
        """<Copy> event handler"""
        self.ttytext.clipboard_append(self.ttytext.get('sel.first', 'sel.last'))
        # I created this function because I was going to make a custom textbox

    def on_paste(self, event):
        """<Paste> event handler"""
        self.ttytext.insert(tk.INSERT, self.ttytext.clipboard_get())
        # I created this function because I was going to make a custom textbox

    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)
        self.ttytext.inst_trigger()


if __name__ == '__main__':
    root = tk.Tk()
    main_window = Console(root)
    main_window.pack(fill=tk.BOTH, expand=True)
    main_window.ttytext.focus_force()
    root.mainloop()

上面的代码使用了jupyter qtconsole(因为它很方便),否则简单的python shell也可以使用code模块中的InteractiveShell()Enter 键、UpDown 箭头键的功能我还没有完全实现。这些可以由用户根据自己的选择进行。

这也可以在 Oli 的回答 here 中找到,并且可以自定义。