Home>

Display the screen with Tkinter and change the status of the widget in another thread.

Create threads and change the status of multiple widgets.
Then, when I press the close button of the window, there is no response.

Is it because there is too much processing?

Increasing the value of time.sleep () made it a little lighter,
I don't want to increase this value too much.
(Should we spawn more threads to distribute the process?)

Is there a workaround?

Windows.

import threading
import time
import tkinter as tk

def func ():
    global flag, labels
    cnt = 0
    while flag:
        for i in range (150):
            labels [i] ["text"] = str (cnt)
        cnt + = 1
        time.sleep (0.1)
def close ():
    global flag, thread, win
    flag = False
    thread.join ()
    win.destroy ()

win = tk.Tk ()
win.geometry ("200x100")
labels = []
for i in range (15):
    for j in range (10):
        label = tk.Label (win)
        label.grid (row = i, column = j)
        labels.append (label)
flag = True
thread = threading.Thread (target = func)
thread.start ()
win.protocol ("WM_DELETE_WINDOW", close)
win.mainloop ()

If i remove thread.join (), it will not be unresponsive,
When I try to process a thread after closing the window, an error occurs,
It behaves as if it doesn't end with VS Code (attached image).

Changed to handle tkinter widget operation in main thread
It is not working well (the display of tkinter does not change even if the value of test.csv is changed → the data transfer part by the queue is suspicious)

import csv
import os
import queue
import threading
import time
import tkinter as tk
import sys
def main ():
    flag = True
    data = queue.Queue ()
    win = tk.Tk ()
    win.geometry ("200x100")
    def loop ():
        nonlocal flag, data
        while flag:
            file = os.path.dirname (sys.argv [0]) + "/test.csv"
            tmp = []
            with open (file, "r", encoding = "utf8") as rf:
                f = csv.reader (rf, lineterminator = "\ r \ n")
                for row in f:
                    tmp.append (row [1])
            #print (tmp)
            data.put (tmp)
            time.sleep (0.001)
    def close ():
        nonlocal flag, thread
        flag = False
        thread.join ()
        win.destroy ()
    win.protocol ("WM_DELETE_WINDOW", close)
    labels = []
    for i in range (4):
        for j in range (5):
            label = tk.Label (win)
            label.grid (row = i, column = j)
            labels.append (label)
    thread = threading.Thread (target = lambda: loop ())
    thread.start ()
    while True:
        while not data.empty ():
            tmp = data.get ()
            print (tmp)
            for i in range (20):
                if not flag: if not flag:
                    return
                labels [i] ["text"] = tmp [i]
                win.update_idletasks ()
            else: else:
                win.update ()
            time.sleep (0.001)

if __name__ =='__main__':
    main ()

test.csv

1,100
2,100
3,100
4,100
5,100
6,100
7,100
8,100
9,100
10,100
11,100
12,100
13,100
14,100
15,100
16,100
17,100
18,100
19,100
20,100
  • Answer # 1

    It is a code that can not be terminated unless it is closed at the timing of sleep.

    There are multiple issues, so let's look at them individually.
    There are two problems,


    I'm making a lot of widgets, so the end is slow

    It's a little lighter → It takes time to finish because there are a lot of widgets.

    Code that reproduces the problem that takes a long time to finish

    import tkinter as tk
    root = tk.Tk ()
    for _ in range (20000): #<--- Please adjust the value here as appropriate.
        label = tk.Label (root)
        label.place (x = 0, y = 0)
        label.place_forget ()
    else: else:
        print (label)
    tk.Button (root, text = "destroy", command = root.destroy) .pack ()
    tk.Button (root, text = "quit", command = root.quit) .pack ()
    root.mainloop ()

    The label in Python will be overwritten in the next loop,
    On the tk side, the label remains.

    destroy will try to destroy all widgets before the program ends

    quit ends the event loop first.

    A workaround for this is to use root.quit (),
    As a workaround, if you implement your own widget using Canvas,
    You only need one widget.


    Since the GUI is operated from the thread, deadlock at the end

    Apart from that, the reason why you can't finish without a response is
    Because GUI operation is performed in the thread.

    The code is such that it cannot be terminated unless it is closed during sleep.

    If you increase sleep, you will be able to quit.
    This is because the probability of pressing close during sleep has increased.
    It's just difficult to reproduce, and depending on the timing, there will be no response.


    To reproduce the problem in an easy-to-understand manner
    I put sleep before and after changing the label.

    sleep 3 seconds<--- before text: If you close here

    Label change (at the time of thread.join) Deadlock that GUI event cannot be processed

    sleep 3 seconds<--- after text: If you close here

    flag end check Normal end

    import time
    import threading
    import tkinter as tk
    import loggingflag = True
    def worker ():
        global flag
        for i in range (5):
            logging.debug ("before text")
            time.sleep (3)
            # Deadlock when closed here (no response)
            # To execute GUI operation related code in a thread
            # Mainloop () must be running on the main thread.
            #
            # But in the main thread, thread.join is waiting for the thread to end
            On the #subthread side, the main thread is waiting for processing to return to the event loop.
            # Neither will end in a standby state, so there will be no response.
            label ["text"] = str (i)
            logging.debug (f "text = {i}")
            # If you close here, it ends normally
            logging.debug ("after text")
            time.sleep (3)
            if not flag: if not flag:
                break
    logging.basicConfig (
        level = logging.DEBUG,
        format = "% (threadName) s% (message) s")
    root = tk.Tk ()
    label = tk.Label (root)
    label.pack ()
    thread = threading.Thread (target = worker)
    thread.start ()
    def close ():
        global flag
        flag = False
        # Blocking process waiting for thread termination
        logging.debug ("thread join")
        thread.join ()
        # This will not run until the thread ends
        tkinter event loop stopped by # thread.join
        logging.debug ("root destroy")
        root.destroy ()
    root.protocol ("WM_DELETE_WINDOW", close)
    root.mainloop ()

    Unthread-safe operation from subthread (label update part)
    This can be avoided by exclusive control of, but the code becomes complicated.
    Queues make things a little simpler, though.

    The workaround for this is to implement it using a GUI event loop timer.

    Basically, only threads with an event loop can update the GUI drawing,
    It's not a problem that can be solved by increasing the number of threads
    It is also not a good thread usage.

    The thread only performs operations (in the GUI-independent part) and
    The less problematic way to use it is to leave the drawing to GUI events.


  • Answer # 2

    Do not use mainloop () intentionally
    Call update ()/update_idletasks () frequently at any time to
    It seems to realize smooth animation without delaying the event

    It's selfless, but I'll post the code to check the operation.
    It's not a solution to your question

    Note: Use loops and time.sleep () in the main thread
    Because general GUI programming
    It's better to avoid it in (event-driven programming in general).

    In this case, while True: ... itself acts as an event loop.
    The main loop will not be used.

    Another note:
    About the position of the judgment to exit the loop with flag
    The WM_DELETE_WINDOW function is called in this event loop
    Inside update/update_idletasks.

    Flag judgment is now done immediately after close () is called
    Place the end condition of the loop.

    Attempting to update the label after win.destroy () is called will result in an error.

    import time
    import tkinter as tk
    def main ():
        flag = True
        cnt = 0
        win = tk.Tk ()
        win.geometry ("200x100")
        def close ():
            nonlocal flag
            flag = False
            win.destroy ()
        win.protocol ("WM_DELETE_WINDOW", close)
        labels = []
        for i in range (15):
            for j in range (10):
                label = tk.Label (win)
                label.grid (row = i, column = j)
                labels.append (label)
        while True:
            for i in range (150):
                if not flag: if not flag:
                    return
                labels [i] ["text"] = cnt
                win.update_idletasks ()
            else: else:
                win.update ()
            cnt + = 1
            time.sleep (0.1)
    if __name__ =='__main__':
        main ()