How can I prevent a tkinter Gui from freezing while an async task is running?

Question:

I want to create a non blocking Gui with tkinter. The way I have seen it so far, you can do as with a mutliprocess. But now I have the problem that I want to access the mainloop of the gui again with the newly created thread and I always get an error here. can you jump back and forth between two threads or is there another method to not block the Gui?

import asyncio
import tkinter as tk 
import multiprocessing as mp 

class pseudo_example():


    def app(self):
        self.root = tk.Tk()
        self.root.minsize(100,100)

        start_button = tk.Button(self.root, text="start", command=lambda: mp.Process(target=self.create_await_fun).start())
        start_button.pack()  #

        self.testfield = tk.Label(self.root, text="test")
        self.testfield.pack()

        #self.root.update_idletasks()
        self.root.mainloop()

    def create_await_fun(self):
        asyncio.run(self.await_fun())

    async def await_fun(self):
        self.root.update_idletasks()
        self.testfield["text"] = "start waiting"
        await asyncio.sleep(2)
        self.testfield["text"] = "end waiting"



if __name__ == '__main__':
    try:
        gui = pseudo_example()
        gui.app()
    except KeyboardInterrupt:
        print("Interrupted")
        sys.exit()

Error message:

[xcb] Unknown sequence number while processing queue
[xcb] Most likely this is a multi-threaded client and XInitThreads has not been called
[xcb] Aborting, sorry about that.
XIO: fatal IO error 0 (Success) on X server ":0"
after 401 requests (401 known processed) with 0 events remaining.
python3.8: ../../src/xcb_io.c:259: poll_for_event: Assertion `!xcb_xlib_threads_sequence_lost’ failed.

i know that the after() method exists but i don’t know how to use it with asyncio without starting the asyncio task. Asyncio is unnecessary in the minimal example but I need it for another application.

Asked By: lu3si

||

Answers:

Tkinter doesn’t support multi-task/multithreading.
You could use mtTkinter which provides thread safety when using multi-tasking: https://pypi.org/project/mttkinter/

You could also use a queue system to transfur data between two functions however you can’t do anything with tkinter objects that cross between the two threads.
Using Queue with tkinter (and threading)

Idk whether this helps. Probably better than using asyncio.

ItzTheDodo.

Answered By: ItzTheDodo

I usually use queues, I place you a sample.In the mask you will
see on the status bar change the time, which is managed by a separate thread
and by consulting the thread tail, see the class Clock, while you can use the various buttons without the mask freezing.I think you can take inspiration from this example.

#!/usr/bin/python3
import sys
import threading
import queue
import datetime
import time
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox

class Clock(threading.Thread):
    def __init__(self):
        threading.Thread.__init__(self)

        self.queue = queue.Queue()
        self.check = True

    def stop(self):
        self.check = False

    def run(self):

        """Feeds the tail."""

        while self.check:
            s = "Astral date: "
            t = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
            msg = "{0} {1}".format(s, t)
            time.sleep(1)
            self.queue.put(msg)
    
    def check_queue(self, obj):

        """Returns a formatted string representing time.
           obj in this case is the statusbar text"""

        while self.queue.qsize():
            try:
                x = self.queue.get(0)
                msg = "{0}".format(x)
                obj.set(msg)
            except queue.Empty:
                pass
            

class Main(ttk.Frame):
    def __init__(self, parent, ):
        super().__init__(name="main")

        self.parent = parent
        self.text = tk.StringVar()
        self.spins = tk.IntVar()
        self.option = tk.IntVar()
        self.check = tk.BooleanVar()
        self.values = ('Apple','Banana','Orange')
        self.status_bar_text = tk.StringVar()
        self.init_status_bar()
        self.init_ui()

    def init_status_bar(self):

        self.status = tk.Label(self,
                               textvariable=self.status_bar_text,
                               bd=1,
                               relief=tk.SUNKEN,
                               anchor=tk.W)
        self.status.pack(side=tk.BOTTOM, fill=tk.X)        
          
    def init_ui(self):

        f0 = ttk.Frame(self)
        f1 = ttk.Frame(f0,)

        ttk.Label(f1, text = "Combobox").pack()
        self.cbCombo = ttk.Combobox(f1,state='readonly',values=self.values)
        self.cbCombo.pack()
        
        ttk.Label(f1, text = "Entry").pack()
        self.txTest = ttk.Entry(f1, textvariable=self.text).pack()

        ttk.Label(f1, text = "Spinbox").pack()
        tk.Spinbox(f1, from_=0, to=15, textvariable= self.spins).pack()

        ttk.Label(f1, text="Checkbutton:").pack()
        ttk.Checkbutton(f1,
                       onvalue=1,
                       offvalue=0,
                       variable=self.check).pack()


        ttk.Label(f1, text="Radiobutton:").pack()
        for index, text in enumerate(self.values):
            ttk.Radiobutton(f1,
                            text=text,
                            variable=self.option,
                            value=index,).pack()
            

        ttk.Label(f1, text="Listbox:").pack()
        self.ListBox = tk.Listbox(f1)
        self.ListBox.pack()
        self.ListBox.bind("<<ListboxSelect>>", self.on_listbox_select)
        self.ListBox.bind("<Double-Button-1>", self.on_listbox_double_button)
        
              
        f2 = ttk.Frame(f0,)

        bts = [("Callback", 7, self.on_callback, "<Alt-k>"),
               ("Args", 0, self.on_args, "<Alt-a>"),
               ("kwargs", 1, self.on_kwargs, "<Alt-w>"),
               ("Set", 0, self.on_set, "<Alt-s>"),
               ("Reset", 0, self.on_reset, "<Alt-r>"),
               ("Close", 0, self.on_close, "<Alt-c>")]

        for btn in bts:
            ttk.Button(f2,
                       text=btn[0],
                       underline=btn[1],
                       command = btn[2]).pack(fill=tk.X, padx=5, pady=5)
            self.parent.bind(btn[3], btn[2])
            
        f1.pack(side=tk.LEFT, fill=tk.BOTH, expand=1)
        f2.pack(side=tk.RIGHT, fill=tk.Y, expand=0)
        f0.pack(fill=tk.BOTH, expand=1)

    def on_open(self):
        self.periodic_call()
        
    def on_callback(self, evt=None):

        print ("self.cbCombo = {}".format(self.cbCombo.get()))
        print ("self.text = {}".format(self.text.get()))
        print ("self.spins = {}".format(self.spins.get()))
        print ("self.check = {}".format(self.check.get()))
        print ("self.option = {}".format(self.option.get()))
        if self.ListBox.curselection():
            print("ListBox.curselection = {}".format(self.ListBox.curselection()[0]))
        else:
            print("{0}".format("No selected item on listbox"))

    def on_args(self, evt=None):

        print("args type: {}".format(type(self.master.args)))
        for p, i in enumerate(self.master.args):
            print(p, i)

    def on_kwargs(self, evt=None):

        print("kwargs type: {}".format(type(self.master.kwargs)))
        for k, v in self.master.kwargs.items():
            print("{0}:{1}".format(k,v))

    def on_reset(self, evt=None):
        self.text.set('')
        self.spins.set(0)
        self.check.set(0)

    def on_set(self, evt=None):
        self.cbCombo.current(1)
        self.text.set('qwerty')
        self.spins.set(42)
        self.check.set(1)
        self.option.set(1)
        self.ListBox.delete(0, tk.END)
        
        for i in self.values:
            s = "{0}".format(i,)
            self.ListBox.insert(tk.END, s)
     
        self.ListBox.selection_set(1)

    def on_listbox_select(self, evt=None):

        if self.ListBox.curselection():

            index = self.ListBox.curselection()
            s = self.ListBox.get(index[0])
            print("on_listbox_select: index = {0} values = {1}".format(index, s))

    def on_listbox_double_button(self, evt=None):

        if self.ListBox.curselection():
            index = self.ListBox.curselection()
            s = self.ListBox.get(index[0])
            print("on_listbox_double_button: index = {0} values = {1}".format(index, s))

    def periodic_call(self):
        """This funciont check the data returned from the clock class queue."""

        self.parent.clock.check_queue(self.status_bar_text)
        
        if self.parent.clock.is_alive():
            self.after(1, self.periodic_call)
        else:
            pass                    
        
    def on_close(self, evt=None):
        self.parent.on_exit()


class App(tk.Tk):
    """Main Application start here"""
    def __init__(self, *args, **kwargs):
        super().__init__()

        self.args = args
        self.kwargs = kwargs
        self.protocol("WM_DELETE_WINDOW", self.on_exit)
        self.set_style(kwargs["style"])  
        self.set_title(kwargs["title"])
        self.resizable(width=False, height=False)

        #start clock on a separate thread...
        self.set_clock()
        
        w = Main(self)
        w.on_open()
        w.pack(fill=tk.BOTH, expand=1)

    def set_clock(self,):
        self.clock = self.get_clock()
        self.clock.start()
        
    def get_clock(self,):
        """Instance the clock."""
        return Clock()
    
    def set_style(self, which):
        self.style = ttk.Style()
        self.style.theme_use(which)
        
    def set_title(self, title):
        s = "{0}".format(title)
        self.title(s)
        
    def on_exit(self):
        """Close all"""
        msg = "Do you want to quit?"
        if messagebox.askokcancel(self.title(), msg, parent=self):
            #stop the thread
            if self.clock is not None:
                self.clock.stop()
            self.destroy()

def main():

    args = []

    for i in sys.argv:
        args.append(i)

    #('winnative', 'clam', 'alt', 'default', 'classic', 'vista', 'xpnative')
    kwargs = {"style":"clam", "title":"Simple App",}

    app = App(*args, **kwargs)

    app.mainloop()

if __name__ == '__main__':
    main()            
    

The main app

enter image description here

Answered By: 1966bc

With respect to https://stackoverflow.com/a/47920128/15959848 I solved my problem. I have created an additional thread so that the GUI and the function each have a thread in which they can be executed.

EDIT Implementation of the shortened version of @Александр

class pseudo_example():

    def __init__(self):
        self.root = tk.Tk()
        self.root.minsize(100, 100)


    def app(self,):
        self.start_button = tk.Button(self.root, text="start", command=lambda: self.create_await_funct())
        self.start_button.pack()

        self.testfield = tk.Label(self.root, text="output")
        self.testfield.pack()
        self.root.mainloop()

    def create_await_funct(self):
        threading.Thread(target=lambda loop: loop.run_until_complete(self.await_funct()),
                         args=(asyncio.new_event_loop(),)).start()
        self.start_button["relief"] = "sunken"
        self.start_button["state"] = "disabled"

    async def await_funct(self):
        self.testfield["text"] = "start waiting"
        self.root.update_idletasks()

        await asyncio.sleep(2)

        self.testfield["text"] = "end waiting"
        self.root.update_idletasks()

        await asyncio.sleep(1)

        self.testfield["text"] = "output"
        self.root.update_idletasks()
        self.start_button["relief"] = "raised"
        self.start_button["state"] = "normal"


if __name__ == '__main__':
    pseudo_example().app()
Answered By: lu3si

An example with async-tkinter-loop library (written by me):

import asyncio
import tkinter as tk 
import sys

from async_tkinter_loop import async_handler, async_mainloop


class pseudo_example():
    def app(self):
        self.root = tk.Tk()
        self.root.minsize(100,100)

        start_button = tk.Button(self.root, text="start", command=async_handler(self.await_fun))
        start_button.pack()

        self.testfield = tk.Label(self.root, text="test")
        self.testfield.pack()

        async_mainloop(self.root)

    async def await_fun(self):
        self.testfield["text"] = "start waiting"
        await asyncio.sleep(2)
        self.testfield["text"] = "end waiting"


if __name__ == '__main__':
    gui = pseudo_example()
    gui.app()
Answered By: insolor
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.