Как мога да вградя рамка за интерпретатор на python в python с помощта на tkinter?

Искам да добавя приспособление за контролен терминал към моето чисто приложение на python+tkinter, подобно на интерпретатора на python, предоставен в Blender. Той трябва да работи в същия контекст (процес), така че потребителят да може да добавя функции и да контролира приложението, което в момента се изпълнява от контролния модул. В идеалния случай бих искал също да "отвлече" 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

person Ralph Ritoch    schedule 16.02.2014    source източник
comment
възможен дубликат на stackoverflow.com/questions/21603038 /   -  person markcial    schedule 16.02.2014
comment
Този проблем е подобен, но този въпрос е как да се направи интерактивен терминал в tkinter Frame и да се прихванат stdout и stderr, като споменаването на stdin е правописна грешка, която ще поправя.   -  person Ralph Ritoch    schedule 16.02.2014
comment
IDLE има нещо подобно, наречено прозорец на Python Shell< /i> и можете да прочетете неговия изходен код.   -  person martineau    schedule 16.02.2014
comment
Повечето от проблемите вече са решени. Трябваше да внедря собствен stdin, от който интерпретаторът чете, и също да обработвам ключови събития и да въвеждам събития в текстовия уиджет, който също направих само за четене. Все още имам проблеми с бягащите нишки. Натискам quit() към интерпретатора, когато прозорецът се затвори, което понякога работи. Също така не знам как да затворя прозореца на конзолата на quit(). Нишката също така изглежда никога не се връща от shell.interact(), което вероятно е причината някои нишки да ми бягат.   -  person Ralph Ritoch    schedule 16.02.2014
comment
това е много интересно, благодаря! обаче черупката не изглежда да отговаря на никаква команда.. и при вас ли е така?   -  person magicrebirth    schedule 20.08.2015


Отговори (2)


Имам отговора, в случай че някой все още го интересува! (Аз също смених на 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, който изпълнява конзолата в текстовия модул:

# 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 е в отделни нишки, е, че методът за четене блокира, което кара програмата да замръзне, докато подпроцесът console.py даде повече изход, освен ако те не са в отделни нишки. Методът writeLoop и опашките са необходими за запис в текстовия модул, тъй като tkinter не е безопасен за нишки.

Това със сигурност все още има проблеми, които трябва да бъдат изгладени, като например факта, че всеки код на джаджата за текст може да се редактира дори след като вече е изпратен, но се надяваме, че отговаря на въпроса ви.

РЕДАКТИРАНЕ: Също така подредих някои от tkinter, така че конзолата да се държи по-скоро като стандартна джаджа.

person Oli    schedule 03.10.2017
comment
Тествах го, работи страхотно! Един въпрос, който имам, е как разбрахте кои функции трябва да замените? Като например за readFromProcessOut, не виждам как работи, къде е входната точка. Също така се чудя дали е възможно да има отделни джаджи за вход и изход. - person Sertalp Bilal; 19.10.2017
comment
@Sertalp Bilal Единствените методи, които се отменят, са __init__ и destroy. Всички други методи се извикват, обвързват с ключове или се стартират в нови нишки в моя код. напр. readFromProcessOut се стартира в нова нишка близо до дъното на метода __init__. Поради начина, по който направих това, би било напълно възможно да има отделни входни и изходни джаджи - това ще изисква малко преструктуриране на кода, но докато и двата джаджа имат препратка към един и същ subproccess обект, ще работи ( можете да зададете нов въпрос за това, нямам нищо против да отговоря) - person Oli; 20.10.2017

не отговаря на команди

Причината, поради която не отговаря на команди, е, че не сте свързали Text приспособлението (self.ttyText) към stdin. В момента, когато пишете, той добавя текст в джаджата и нищо друго. Това свързване може да се направи подобно на това, което вече сте направили с stdout и stderr.

Когато прилагате това, трябва да следите коя част от текста в изпълнимия модул е ​​текстът, въведен от потребителя - това може да се направи с помощта на знаци (както е описано тук).

нишката не спира, когато потребителят затвори прозореца.

Не мисля, че има "чист" начин за решаване на този проблем без основно презаписване на кода, но решение, което изглежда работи достатъчно добре, е просто да открие кога джаджата е унищожена и да напише низа "\n\nexit()" в интерпретатора . Това извиква функцията 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

person Oli    schedule 06.11.2019