Aamir Rind
Aamir Rind

Reputation: 39659

tkinter tkMessageBox not working in thread

i have tkinter class and some functions in it, (assume all other functions are present to initiate the GUI). what i have done i have started one self.function as a thread from other self.function and in threaded function upon error i want to use tkMessageBox.showerror('Some Error') but this does not work in threaded function and my program got stuck. msgbox is working in other function.

import threading
from Tkinter import *
import Pmw
import tkMessageBox

class tkinter_ui:
      def __init__(self, title=''):
      ... assume all functions are present ...

      def login(self, username, password)
          if password == "":
             tkMessageBox.showerror('Login Error', 'password required') # but on this msg box program become unresponsive why???

      def initiateLogin(self)
          tkMessageBox.showinfo('Thread', 'Started')   #you see this msg box works
          self.t = threading.Timer(1, self.login)
          self.t.start()

Upvotes: 2

Views: 4784

Answers (3)

Ohad Cohen
Ohad Cohen

Reputation: 6144

If you want your other thread to block until you get response (e,g: you want to ask a question and wait for the answer) you can use this function:

def runInGuiThreadAndReturnValue(self, fun, *args, **kwargs):
    def runInGui(fun, ret, args, kwargs):
        ret.append(fun( *args, **kwargs))
    ret = []
    sleeptime = kwargs.pop('sleeptime', 0.5)
    self.after(0, runInGui, fun, ret, args, kwargs)
    while not ret:
        time.sleep(sleeptime)
    return ret[0]

Upvotes: 0

etuardu
etuardu

Reputation: 5516

Since I got stuck on the same problem and didn't find a proper, well explained solution, I'd like to share a basic strategy I came out with.

Note that this is not the only nor the best way to do threading with tkinter, but it's quite straightforward and should preserve your workflow if you designed your code without being aware of tkinter's thread-unsafetiness.

Why threads?

First of all, I chose to use threads seeing that blocking actions like os.popen, subprocess.call, time.sleep and the like would "freeze" the GUI until they run (of course this may not be your case since threads are useful by their own for many reasons and sometimes they are just needed).

This is how my code looked like before using threads:

from Tkinter import *
import tkMessageBox
from time import sleep

# Threadless version.
# Buttons will freeze the GUI while running (blocking) commands.

def button1():
    sleep(2)
    tkMessageBox.showinfo('title', 'button 1')

def button2():
    sleep(2)
    tkMessageBox.showinfo('title', 'button 2')

root = Tk()
frame = Frame(root)
frame.pack()

Frame(root).pack( side = BOTTOM )
Button(frame, command=button1, text="Button 1").pack( side = LEFT )
Button(frame, command=button2, text="Button 2").pack( side = LEFT )
root.mainloop()

Buggy threaded version

Then I turned the commands called by the buttons into threads. This way, the GUI would not freeze.

I thought it was ok, but on Windows this code leads the interpreter to crash irreparably due to the tkMessageBoxes called from threads other than the one in which the tkinter's root is running:

from Tkinter import *
import tkMessageBox
from time import sleep
import threading

# Buggy threads.
# WARNING: Tkinter commands are run into threads: this is not safe!!!

def button1():
    sleep(2)
    tkMessageBox.showinfo('title', 'button 1')

def button2():
    sleep(2)
    tkMessageBox.showinfo('title', 'button 2')

def start_thread(fun, a=(), k={}):
    threading.Thread(target=fun, args=a, kwargs=k).start()

root = Tk()
frame = Frame(root)
frame.pack()

Frame(root).pack( side = BOTTOM )
Button(frame, command=lambda: start_thread(button1), text="Button 1").pack( side = LEFT)
Button(frame, command=lambda: start_thread(button2), text="Button 2").pack( side = LEFT )
root.mainloop()

Thread-safe version

When I discovered the thread-unsafetiness of tkinter, I wrote a small function tkloop that would run in the main thread each few milliseconds checking requests and executing requested (tkinter) functions on behalf of the threads that wish to run them.

The two keys here are the widget.after method that "registers a callback function that will be called after a given number of milliseconds" and a Queue to put and get requests.

This way, a thread can just put the tuple (function, args, kwargs) into the queue instead of calling the function, resulting in a unpainful change of the original code.

This is the final, thread-safe version:

from Tkinter import *
import tkMessageBox
from time import sleep
import threading
from Queue import Queue

# Thread-safe version.
# Tkinter functions are put into queue and called by tkloop in the main thread.

q = Queue()

def button1():
    sleep(2)
    q.put(( tkMessageBox.showinfo, ('title', 'button 1'), {} ))

def button2():
    sleep(2)
    q.put(( tkMessageBox.showinfo, ('title', 'button 2'), {} ))

def start_thread(fun, a=(), k={}):
    threading.Thread(target=fun, args=a, kwargs=k).start()

def tkloop():
    try:
        while True:
            f, a, k = q.get_nowait()
            f(*a, **k)
    except:
        pass

    root.after(100, tkloop)


root = Tk()
frame = Frame(root)
frame.pack()

Frame(root).pack( side = BOTTOM )
Button(frame, command=lambda: start_thread(button1), text="Button 1").pack( side = LEFT)
Button(frame, command=lambda: start_thread(button2), text="Button 2").pack( side = LEFT )
tkloop() # tkloop is launched here
root.mainloop()

Edit: two-way communication: if your threads need to get informations from the main (e.g. return values from tkinter functions) you can edit the interface of tkloop adding a queue for the return values. Here's an example based on the code above:

def button1():
    q1 = Queue()
    sleep(2)
    q.put(( tkMessageBox.askokcancel, ('title', 'question'), {}, q1 ))
    response = 'user said ' + 'OK' if q1.get() else 'CANCEL'
    q.put(( tkMessageBox.showinfo, ('title', response), {}, None ))

# ...

def tkloop():
    try:
        while True:
            f, a, k, qr = q.get_nowait()
            r = f(*a, **k)
            if qr: qr.put(r)
    except:
        pass

    root.after(100, tkloop)

Upvotes: 9

Bryan Oakley
Bryan Oakley

Reputation: 385960

tkinter is not thread safe -- you can't reliably call any tkinter functions from any thread other than the one in which you initialized tkinter.

Upvotes: 5

Related Questions