Tkinter locks Python when an icon is loaded and tk.mainloop is in a thread
Asked Answered
C

1

10

Here's the test case...

import Tkinter as tk
import thread
from time import sleep

if __name__ == '__main__':
    t = tk.Tk()
    thread.start_new_thread(t.mainloop, ())
    # t.iconbitmap('icon.ico')

    b = tk.Button(text='test', command=exit)
    b.grid(row=0)

    while 1:
        sleep(1)

This code works. Uncomment the t.iconbitmap line and it locks. Re-arrange it any way you like; it will lock.

How do I prevent tk.mainloop locking the GIL when there is an icon present?

The target is win32 and Python 2.6.2.

Capillarity answered 29/7, 2009 at 5:57 Comment(0)
Q
23

I believe you should not execute the main loop on a different thread. AFAIK, the main loop should be executed on the same thread that created the widget.

The GUI toolkits that I am familiar with (Tkinter, .NET Windows Forms) are that way: You can manipulate the GUI from one thread only.

On Linux, your code raises an exception:

self.tk.mainloop(n)
RuntimeError: Calling Tcl from different appartment

One of the following will work (no extra threads):

if __name__ == '__main__':
    t = tk.Tk()
    t.iconbitmap('icon.ico')

    b = tk.Button(text='test', command=exit)
    b.grid(row=0)

    t.mainloop()

With extra thread:

def threadmain():
    t = tk.Tk()
    t.iconbitmap('icon.ico')
    b = tk.Button(text='test', command=exit)
    b.grid(row=0)
    t.mainloop()


if __name__ == '__main__':
    thread.start_new_thread(threadmain, ())

    while 1:
        sleep(1)

If you need to do communicate with tkinter from outside the tkinter thread, I suggest you set up a timer that checks a queue for work.

Here is an example:

import Tkinter as tk
import thread
from time import sleep
import Queue

request_queue = Queue.Queue()
result_queue = Queue.Queue()

def submit_to_tkinter(callable, *args, **kwargs):
    request_queue.put((callable, args, kwargs))
    return result_queue.get()

t = None
def threadmain():
    global t

    def timertick():
        try:
            callable, args, kwargs = request_queue.get_nowait()
        except Queue.Empty:
            pass
        else:
            print "something in queue"
            retval = callable(*args, **kwargs)
            result_queue.put(retval)

        t.after(500, timertick)

    t = tk.Tk()
    t.configure(width=640, height=480)
    b = tk.Button(text='test', name='button', command=exit)
    b.place(x=0, y=0)
    timertick()
    t.mainloop()

def foo():
    t.title("Hello world")

def bar(button_text):
    t.children["button"].configure(text=button_text)

def get_button_text():
    return t.children["button"]["text"]

if __name__ == '__main__':
    thread.start_new_thread(threadmain, ())

    trigger = 0
    while 1:
        trigger += 1

        if trigger == 3:
            submit_to_tkinter(foo)

        if trigger == 5:
            submit_to_tkinter(bar, "changed")

        if trigger == 7:
            print submit_to_tkinter(get_button_text)

        sleep(1)
Questionary answered 29/7, 2009 at 6:9 Comment(4)
Well you've hit the nail on the head, it works... but I've suffered from not providing enough information. My reasoning is I want to be able to do things to tkinter where the while loop is... Being a bit new to SO, should I accept your answer and ask another, more verbose question?Capillarity
Hi, I've updated my answer with a suggestion and code example to that accomplishes this. The while loop now calls a few method on the tkinter thread, using request/response queues.Questionary
BTW, for production code I suggest you encapsulate the Tkinter window, thread and queues in a class. This to avoid the globals we now have: request_queue, response_queue and t. You also need some error handling around callable(*args, **kwargs).Questionary
I would've been happy with a simple "You should be using a queue for that", you've gone well beyond the call of duty, thank you.Capillarity

© 2022 - 2024 — McMap. All rights reserved.