Alex
Alex

Reputation: 21

Change to non-modal in tkinter

How can I change it to non-modal ? If button is clicked then cursor doesn't change. If I change it to non-modal then it should be ok.

def start_new_proc():
    # text.config(cursor="clock")
    root.config(cursor="clock")
    text.config(state="normal")
    command = "ping 8.8.8.8 -c 1"
    proc = Popen(command, shell=True, stdout=PIPE)
    for line in iter(proc.stdout.readline, ''):
        line = line.decode('utf-8')
        if line == '':
            break
        text.insert("end", line)
    proc.wait()
    # text.config(cursor="")
    root.config(cursor="")
    text.config(state="disable")



root = tk.Tk()
text = tk.Text(root, state="disabled")
text.pack()

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

root.mainloop()

Upvotes: 0

Views: 515

Answers (2)

Matiiss
Matiiss

Reputation: 6156

Here is one approach, it uses threading and queue to not block the .mainloop() and to not call tkinter methods from another thread (which may potentially break it):

import tkinter as tk
from subprocess import Popen, PIPE
from threading import Thread
from queue import Queue


def start_new_proc():
    # text.config(cursor="clock")
    root.config(cursor="clock")
    text.config(state="normal")
    queue = Queue()
    Thread(target=lambda: run_proc(queue)).start()
    update_text(queue)


def after_proc():
    # text.config(cursor="")
    root.config(cursor="")
    text.config(state="disable")


def update_text(queue):
    data = queue.get()
    if data == 'break':
        after_proc()
        return
    text.insert('end', data)
    root.after(100, update_text, queue)


def run_proc(queue):
    command = "ping 8.8.8.8"
    proc = Popen(command, shell=True, stdout=PIPE)
    for line in iter(proc.stdout.readline, ''):
        line = line.decode('utf-8')
        if line == '':
            queue.put('break')
            break
        queue.put(line)
    # proc.wait()


root = tk.Tk()
text = tk.Text(root, state="disabled")
text.pack()

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

root.mainloop()

Quick explanation:

When you click the button, it calls start_new_proc():

Now first all the configurations are run so now the cursor and text are configured (note that the clock cursor is visible only when the cursor is not on Text widget, since the config for text widget is commented out)

Then a queue object is created, this will allow to safely communicate between threads (and it will get garbage collected once the function "stops")

Then a thread is started where the cmd command is run (oh and I changed it a bit (removed "-c 1") because I couldn't test otherwise). So every iteration the data is put into the queue for the main thread to safely receive it, other stuff is the same, also notice that there is '"break"' put into queue once the loop has to stop, so that the other end knows to stop too (probably better to place some object since "break" can be placed there by line variable)

Then the update_text() function is called which receives data from the queue and then inserts it to the text widget (there is also the "break" check) and then the .after() method for making the function loop without blocking the .mainloop() (and also when "break" apppears then configuration function gets called to config the state and cursor)

Upvotes: 0

Henry
Henry

Reputation: 3942

You can achieve this using threading.

import threading
def ping_func(output):
    command = "ping 8.8.8.8 -c 1"
    proc = Popen(command, shell=True, stdout=PIPE)
    for line in iter(proc.stdout.readline, ''):
        line = line.decode('utf-8')
        if line == '':
            break
        output.append(line)
    

def check_complete():
    if not ping_thread.is_alive():
        root.config(cursor = "")
        button.config(state = "normal")
    else:
        root.after(100, check_complete)

def check_output():
    if output != []:
        text.config(state = "normal")
        text.insert("end", output.pop(0))
        text.config(state = "disabled")
    root.after(100, check_output)

def start_new_proc():
    #text.config(cursor="clock")
    root.config(cursor="clock")
    button.config(state = "disabled") #Disable button until process completed
    global output, ping_thread
    output = []
    ping_thread = threading.Thread(target = ping_func, args = (output,))
    ping_thread.start()
    check_output()
    check_complete()



root = tk.Tk()
text = tk.Text(root, state="disabled")
text.pack()

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

root.mainloop()

By putting the command execution in a thread, it doesn't hold up the tkinter thread. The function ping_func is what the thread runs. It does what the program did before, but the output is different. Because tkinter isn't thread safe, you can't use tkinter objects in other threads. Therefore we need to pass a variable instead which can then be polled by the tkinter thread. This is what the output list is for. The output from ping_func is appended to output. To check the contents of output and insert it into the Text widget, we need a function check_output. This calls itself every 100ms to check if the contents of output have changed. If they have it inserts the new line into the Text widget. There is also check_complete which checks every 100ms if the thread has ended and changes the cursor and enables the button if it has.

Upvotes: 0

Related Questions