0

I have gone through many solutions on stackoverflow, but none was helpful to me. I'm stuck on implementing cmd into tkinter to see output inside of gui and be able to enter values there. I appreciate any help, thanks for advance!

from subprocess import Popen
from tkinter import Tk, Button, messagebox, Label
from PIL import ImageTk, Image


gui = Tk(className='IDPass')
gui.geometry('500x500')
gui.iconbitmap('Turnstile/icons/mini_logo.ico')
img = ImageTk.PhotoImage(Image.open('Turnstile/icons/logo.png'))
panel = Label(gui, image=img)


def run_server():
    global process
    process = Popen(['python', 'C:/Test/Turnstile/manage.py', 'runserver'])

def run_rfid_scanner():
    global process
    process = Popen('python C:/Test/Turnstile/rfid_scanner.py')
    
def run_face_scanner():
    global process
    process = Popen('python C:/Test/Turnstile/face_scanner.py')
    
def run_photo_deleter():
    global process
    process = Popen('python C:/Test/Turnstile/photo_deleter.py')
    
def run_face_recognizer():
    global process
    process = Popen('python C:/Test/Turnstile/face_recognizer.py')

def stop_program():
    process.kill()
    messagebox.showinfo('Информационное окно', 'Программа остановлена')


server = Button(gui, text='Запустить сервер', command=run_server, bg='green')
rfid_scanner = Button(gui, text='Запустить RFID сканер', command=run_rfid_scanner, bg='green')
face_scanner = Button(gui, text='Добавить фото для сканирования', command=run_face_scanner, bg='green')
face_recognizer = Button(gui, text='Начать распознавание лица', command=run_face_recognizer, bg='green')

photo_deleter = Button(gui, text='Удалить фото пользователя', command=run_photo_deleter, bg='grey')
stop_programm = Button(gui, text='Остановить выполнение программы', command=stop_program, bg='grey')

panel.pack()
server.pack()
rfid_scanner.pack()
face_scanner.pack()
face_recognizer.pack()
photo_deleter.pack()
stop_programm.pack()

gui.mainloop()

This is how I want to see it

enter image description here

Artem
  • 15
  • 7
  • Does this answer your question? [Redirect command line results to a tkinter GUI](https://stackoverflow.com/questions/665566/redirect-command-line-results-to-a-tkinter-gui) – PCM Sep 03 '21 at 06:07
  • I've tried that, but it didn't help me unfortunately. Window opens and closes automatically – Artem Sep 03 '21 at 06:08

2 Answers2

1

One of the way is:

  • create a Text box to show the command output
  • create a threaded task to get the process output and put the output in a queue
  • create a periodic task to get output from the queue and insert it into text box
  • redirect command output using subprocess.PIPE
import sys
import threading
from queue import Queue
from subprocess import Popen, PIPE
from tkinter import Tk, Button, messagebox, Label, Text
...
process = None
queue = Queue()

def run_server():
    global process
    if process:
        process.terminate()
    #process = Popen(['python', 'C:/Test/Turnstile/manage.py', 'runserver'])
    process = Popen([sys.executable, '-u', 'C:/Test/Turnstile/manage.py', 'runserver'], stdout=PIPE, bufsize=1, text=True)

...

output = Text(gui, width=100, height=20)
output.pack(padx=20, pady=20)

def monitor_output(q):
    while True:
        if process and process.stdout:
            msg = process.stdout.readline()
            if msg:
                q.put(msg)

def check_output(q):
    while not q.empty():
        output.insert('end', q.get())
        output.see('end')
    gui.after(10, check_output, q)

threading.Thread(target=monitor_output, args=[queue], daemon=True).start()
check_output(queue)

gui.mainloop()

Note that I have used sys.executable instead of 'python' to make sure same Python interpreter is used.

acw1668
  • 40,144
  • 5
  • 22
  • 34
  • This is what I need, thank you! But I have a problem, monitor_output() blocks a thread and my window is not responsible to anything, so I can't even run another functions. How to fix it? – Artem Sep 03 '21 at 07:59
  • `monitor_output()` is just a periodic task using `after()` and it should not block other thread. Also did you modify other `Popen(...)` to use the same options in my answer? – acw1668 Sep 03 '21 at 08:08
  • Yes, I did same with another function, but once I click "Run server" button my gui freezes and I can't press button to run another function. I probably should go with a new thread for that – Artem Sep 03 '21 at 08:30
  • @acw1668 I think that `process.stdout.readline()` is a blocking read. So if the new process is running but isn't outputting to stdout, the `.readline()` will block that thread. – TheLizzard Sep 03 '21 at 08:48
  • @Temik26 Answer updated to use threading to read command output. Used `Queue` to send back the output to main task and use `after()` to read the output periodically. – acw1668 Sep 03 '21 at 09:06
  • **TypeError: empty() missing 1 required positional argument: 'self'** – Artem Sep 03 '21 at 09:15
  • @Temik26 Sorry typo. `queue = Queue` should be `queue = Queue()` instead. – acw1668 Sep 03 '21 at 09:16
  • @acw1668 Btw instead of a `Queue`, you can use a `threading.Lock` and a normal `str`ing. But that's just my preference. – TheLizzard Sep 03 '21 at 09:25
  • @acw1668 Yes, my bad, didn't see it. 1 question: when I'm trying to run face_scanner.py there should be output like enter user id etc., but it doesn't appear in gui for some reason, camera turns on but nothing is in gui, why? With server run everything works great. – Artem Sep 03 '21 at 09:30
  • @Temik26 Console input should be avoided in a GUI application. – acw1668 Sep 03 '21 at 09:31
  • @acw1668 But how can I make it without cmd pop up? – Artem Sep 03 '21 at 09:34
  • @Temik26 So you want both input and output in the console? That is much harder but look at [this](https://github.com/TheLizzard/Bismuth-184/blob/main/src/terminal.pyw). It's still work in progress. – TheLizzard Sep 03 '21 at 09:41
  • @Temik26 I think it is better to modify the script to accept those inputs from command line arguments and then use an input form to get those inputs and pass them as command line arguments when the script is executed. – acw1668 Sep 03 '21 at 09:44
  • @acw1668 Yes, I need input as well. I just want cmd implemented into gui to do everything in gui-cmd instead of cmd. – Artem Sep 03 '21 at 09:49
1

Try this:

from subprocess import Popen, PIPE
from threading import Thread, Lock
import tkinter as tk


class TkinterPopen(tk.Text):
    def __init__(self, master, state="disabled", **kwargs):
        super().__init__(master, state=state, **kwargs)
        self.commands = []
        self.proc = None
        self.running = True
        self.stdout_buffer = ""
        self.stdout_buffer_lock = Lock()

    def stdout_loop(self, last_loop:bool=False) -> None:
        with self.stdout_buffer_lock:
            # Get the data and clear the buffer:
            data, self.stdout_buffer = self.stdout_buffer, ""
        state = super().cget("state")
        super().config(state="normal")
        super().insert("end", data)
        super().see("end")
        super().config(state=state)
        if self.proc is None:
            if len(self.commands) == 0:
                # If we are done with all of the commands:
                if last_loop:
                    return None
                super().after(100, self.stdout_loop, True)
            else:
                # If we have more commands to do call `start_next_proc`
                self.start_next_proc()
        else:
            super().after(100, self.stdout_loop)

    def start_next_proc(self) -> None:
        command = self.commands.pop(0) # Take the first one from the list
        self.proc = Popen(command, stdout=PIPE)
        new_thread = Thread(target=self.read_stdout, daemon=True)
        new_thread.start()
        self.stdout_loop()

    def run_commands(self, commands:list) -> None:
        self.commands = commands
        self.start_next_proc()

    def read_stdout(self):
        while self.proc.poll() is None:
            self._read_stdout()
        self._read_stdout()
        self.proc = None

    def _read_stdout(self) -> None:
        line = self.proc.stdout.readline()
        with self.stdout_buffer_lock:
            self.stdout_buffer += line.decode()


if __name__ == "__main__":
    def start_echo():
        command = ["echo", "hi"]
        tkinter_popen.run_commands([command])

    def start_ping():
        # For linux use "-c". For windows use "-n"
        command = ["ping", "1.1.1.1", "-n", "3"]
        tkinter_popen.run_commands([command])

    root = tk.Tk()

    tkinter_popen = TkinterPopen(root)
    tkinter_popen.pack()

    button = tk.Button(root, text="Run echo", command=start_echo)
    button.pack()

    button = tk.Button(root, text="Run ping", command=start_ping)
    button.pack()

    root.mainloop()

I think this is the functionality that you wanted. The code is similar to @acw1668 but I read stdout in another thread and out the data in a queue named self.stdout_buffer.

This is just a copy of the answer I gave here.

TheLizzard
  • 7,248
  • 2
  • 11
  • 31
  • I just tried to put one of my script in command like this: **commands = [sys.executable, '-u', 'C:/Test/Turnstile/manage.py', 'runserver']** and it opened python interpreter for me. Was it a wrong way? – Artem Sep 03 '21 at 09:05
  • Try `commands = [[sys.executable, '-u', 'C:/Test/Turnstile/manage.py', 'runserver']]`. `commands` is meant to be a list of different commands to run. In the example I put 3 commands just to show how it works. – TheLizzard Sep 03 '21 at 09:10
  • You made 1 button to run all commands as I see, but I need to run it separately, I mean 1 button = 1 program – Artem Sep 03 '21 at 09:28
  • @Temik26 i updated my answer and added another button. – TheLizzard Sep 03 '21 at 09:40
  • Yes, did it. Is it possible to make cmd input as well? Now it's output only – Artem Sep 03 '21 at 09:58
  • @Temik26 Adding user input would make this a whole project. There are many things to consider when adding input. If you want, you can use [this](https://github.com/TheLizzard/Bismuth-184/blob/main/src/terminal.pyw) but it still has bugs and quite a few dependencies, It took me a few weeks to create and there is no way of explaining everything in a stackoverflow answer. Can you change the python files you want to run to GUIs? That would be the easiest approach. – TheLizzard Sep 03 '21 at 10:01
  • I got you. The main reason is that my script should ask a person which gonna manage the system what person id you wanna enter to go further, and id entering is the part that I wanna see as input in gui – Artem Sep 03 '21 at 10:11
  • @Temik26 can't you simply import those scripts, as far as I have seen you use `Popen` only to run other `.py` files, why not simply import those and run the functions you need? – Matiiss Sep 03 '21 at 12:48