0

As I mentioned in the comment lines, the idea behind is like this:

There is a root and a button on it, once the button's clicked there will be a splash screen popping up first while the elements of the top level getting ready but without the root screen getting frozen. Is there any way to make this happen? Thanks in advance!

from tkinter import *
import customtkinter
import threading

def splash_screen():
    global splash_screen
    splash_screen = Tk()

    label = customtkinter.CTkLabel(splash_screen, text="PLEASE WAIT...")
    label.pack(pady=30, padx=30)


def initiate():
    # get elements of the toplevel
    pass

def toplevel():
    # second main window after root
    pass

def func1():
    # to avoid root freezing
    threading.Thread(target=func2).start()

def func2():
    thread = threading.Thread(target=initiate)
    thread.start()

    splash_screen()
    # wait until toplevel is ready
    thread.join()
    splash_screen.destroy()

    toplevel()


root = customtkinter.CTk()

button = customtkinter.CTkButton(root, command=func1)
button.pack(pady=10, padx=10)

root.mainloop()

Traceback:

Exception in Tkinter callback
Traceback (most recent call last):
  File "D:\Python311\Lib\tkinter\__init__.py", line 1948, in __call__
    return self.func(*args)
           ^^^^^^^^^^^^^^^^
  File "D:\Python311\Lib\tkinter\__init__.py", line 861, in callit
    func(*args)
  File "D:\Python311\Lib\site-packages\customtkinter\windows\widgets\scaling\scaling_tracker.py", line 178, in check_dpi_scaling
    if window.winfo_exists() and not window.state() == "iconic":
       ^^^^^^^^^^^^^^^^^^^^^
  File "D:\Python311\Lib\tkinter\__init__.py", line 1139, in winfo_exists
    self.tk.call('winfo', 'exists', self._w))
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: main thread is not in main loop
acw1668
  • 40,144
  • 5
  • 22
  • 34
duruburak
  • 181
  • 14
  • `tkinter`is telling you that it doesn't like being called from multiple threads. Only call `tkinter` from a single thread if you want to avoid problems like this. Also how many widgets do you have that the GUI freezes while it initialises? – TheLizzard Jan 03 '23 at 00:46
  • Does this answer your question? [Tkinter: How to use threads to preventing main event loop from "freezing"](https://stackoverflow.com/questions/16745507/tkinter-how-to-use-threads-to-preventing-main-event-loop-from-freezing) – dskrypa Jan 03 '23 at 00:53
  • @TheLizzard it's uncertain (user-dependant). It could be 10 widgets or 100. – duruburak Jan 03 '23 at 15:11
  • @dskrypa Isn't it the same logic with my code? But in my situation there are multiple windows screens involved. – duruburak Jan 03 '23 at 15:53

1 Answers1

0

Don't manipulate UI objects in a background thread.(Even though the tkinter allows manipulating UI objects in a background thread in some degree unlike other UI frameworks, it's best to avoid that.)

Call after_idle() in a background thread to interact with the main thread, like the following example. With this pattern, you don't need a nested thread.

import time
import threading
import tkinter

worker_thread = None
def worker_entry():
    def update_status(v):
        # This will run in the main UI thread.
        status_text.delete(1.0, 'end')
        status_text.insert('end', v)
    for i in range(30):
        root.after_idle(update_status, f'i:{i}')
        time.sleep(0.1)
    def join_worker():
        # This will run in the main UI thread.
        global worker_thread
        worker_thread.join()
        worker_thread = None
        start_worker_button['state'] = 'normal'
    root.after_idle(join_worker)
def on_start_worker():
    global worker_thread
    worker_thread = threading.Thread(target=worker_entry)
    worker_thread.start()
    start_worker_button['state'] = 'disabled'

root = tkinter.Tk()
start_worker_button = tkinter.Button(root, text='start worker',
    command=on_start_worker)
start_worker_button.pack()  
status_text = tkinter.Text(root)
status_text.pack()

root.mainloop()

As a side note, the above pattern is a de facto standard for UI frameworks. For example, there are the Dispatcher.BeginInvoke() in WPF, the QTimer::singleShot() in Qt, the gdk_threads_add_idle_full() in Gtk, the Activity.runOnUiThread() in Android, etc.

The following is another example covering the scenario of the OP, showing a splash window. The pattern is identical as the above.

import time
import threading
import tkinter

worker_thread = None
def worker_entry():
    def update_status(v):
        text = splash_win.status_text
        text.delete(1.0, 'end')
        text.insert('end', v)
    for i in range(30):
        root.after_idle(update_status, f'i:{i}')
        time.sleep(0.1)
    def join_worker():
        global worker_thread, splash_win
        worker_thread.join()
        worker_thread = None
        splash_win.destroy()
        splash_win = None
        start_worker_button['state'] = 'normal'
    root.after_idle(join_worker)
def on_start_worker():
    global worker_thread, splash_win
    splash_win = win = tkinter.Toplevel(root)
    splash_win.status_text = text = tkinter.Text(win)
    text.pack()
    worker_thread = threading.Thread(target=worker_entry)
    worker_thread.start()
    start_worker_button['state'] = 'disabled'

root = tkinter.Tk()
start_worker_button = tkinter.Button(root, text='start worker',
    command=on_start_worker)
start_worker_button.pack()  

root.mainloop()

As another side note, I experimented a scenario similar to one of the OP, where a second Tk root(Tcl interpreter) is created in the first background thread and found calling after_idle() in the second(nested) background thread causes a problem. By looking at the source, I found that the current implementation does not support this scenario.

relent95
  • 3,703
  • 1
  • 14
  • 17
  • Calling `tkinter` methods from threads other than the one where you create the `tk.Tk` is not advisable. It is possible for `tkinter` to crash python without a traceback making it practically impossible to debug. – TheLizzard Jan 03 '23 at 15:25
  • I tried to adapt `after_idle()` to my situation, still same result: "Main thread is not in mainloop" – duruburak Jan 03 '23 at 15:48
  • @TheLizzard, it's not true. See [the documentation](https://docs.python.org/3/library/tkinter.html#threading-model). It says "Internally, if a call comes from a thread other than the one that created the Tk object, an event is posted to the interpreter’s event queue, ...". If you are interested see [the implementation](https://github.com/python/cpython/blob/3.11/Modules/_tkinter.c#L1424). – relent95 Jan 04 '23 at 00:31
  • @duruburak, did you restructure your code, so it does not use a nested thread and multiple Tk() roots? Anyway, I'll add another example covering your scenario. – relent95 Jan 04 '23 at 00:37
  • @relent95 I used splash screen as second `Tk()` root, so you mean I should use splash screen as `Toplevel()` instead of `Tk()` and there is no other way? – duruburak Jan 04 '23 at 01:29
  • @duruburak, it's possible technically, but what's the merit of it? Using two roots(two interpreters) makes the problem unnecessarily complex. Anyway, I'll add another example for that case to satisfy my own curiosity. – relent95 Jan 04 '23 at 01:50
  • 1
    @duruburak, I experimented a scenario where a second Tk root(interpreter) is created in the 1st background thread and found calling ```after_idle()``` in the 2nd(nested) background thread causes a problem. Also I found the current implementation actually does not support that scenario by looking at the source. In short, stick to the simple model - one Tk root(Tcl interpreter) in the main thread. – relent95 Jan 04 '23 at 05:41
  • @relent95 Look at [this](https://pastebin.com/wF5Fn19a). It crashes python just because you can access a `tkinter` variable from another thread. `tkinter` is not fully thread-safe, look at [this](https://stackoverflow.com/a/14695007/11106801) question where `tkinter` directly raises an error because it is called from another thread. – TheLizzard Jan 04 '23 at 12:20
  • @TheLizzard, yes I know. That's why I recommended to avoid manipulating UI objects in the background thread and only call ```after_idle()```, to minimize side effects. This is an old ugly issue. See [this issue](https://bugs.python.org/issue33479) if you are interested. Ivan said "at least the work item submitting API must be thread-safe" and I am suggesting that API to be ```after_idle()```. – relent95 Jan 05 '23 at 00:49
  • @relent95 And the first link in my last comment shows an example where `tkinter` crashes just because you can access a `tkinter` variable from the 2nd thread. Therefore, even if you don't call a `tkinter` method, `tkinter` can crash. – TheLizzard Jan 05 '23 at 01:54
  • @TheLizzard, that's just a bug of the tkinter, occurs on an extremely rare case. What's the point? It's your choice to not use the imperfect but stable feature if you don't trust it, but don't force others. – relent95 Jan 05 '23 at 02:21