|
@@ -0,0 +1,253 @@
|
|
|
+'''Thread-safe version of Tkinter.
|
|
|
+
|
|
|
+Copyright (c) 2009, Allen B. Taylor
|
|
|
+
|
|
|
+This module is free software: you can redistribute it and/or modify
|
|
|
+it under the terms of the GNU Lesser Public License as published by
|
|
|
+the Free Software Foundation, either version 3 of the License, or
|
|
|
+(at your option) any later version.
|
|
|
+
|
|
|
+This program is distributed in the hope that it will be useful,
|
|
|
+but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+GNU Lesser Public License for more details.
|
|
|
+
|
|
|
+You should have received a copy of the GNU Lesser Public License
|
|
|
+along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
+
|
|
|
+Usage:
|
|
|
+
|
|
|
+ import mtTkinter as Tkinter
|
|
|
+ # Use "Tkinter." as usual.
|
|
|
+
|
|
|
+or
|
|
|
+
|
|
|
+ from mtTkinter import *
|
|
|
+ # Use Tkinter module definitions as usual.
|
|
|
+
|
|
|
+This module modifies the original Tkinter module in memory, making all
|
|
|
+functionality thread-safe. It does this by wrapping the Tk class' tk
|
|
|
+instance with an object that diverts calls through an event queue when
|
|
|
+the call is issued from a thread other than the thread in which the Tk
|
|
|
+instance was created. The events are processed in the creation thread
|
|
|
+via an 'after' event.
|
|
|
+
|
|
|
+The modified Tk class accepts two additional keyword parameters on its
|
|
|
+__init__ method:
|
|
|
+ mtDebug:
|
|
|
+ 0 = No debug output (default)
|
|
|
+ 1 = Minimal debug output
|
|
|
+ ...
|
|
|
+ 9 = Full debug output
|
|
|
+ mtCheckPeriod:
|
|
|
+ Amount of time in milliseconds (default 100) between checks for
|
|
|
+ out-of-thread events when things are otherwise idle. Decreasing
|
|
|
+ this value can improve GUI responsiveness, but at the expense of
|
|
|
+ consuming more CPU cycles.
|
|
|
+
|
|
|
+Note that, because it modifies the original Tkinter module (in memory),
|
|
|
+other modules that use Tkinter (e.g., Pmw) reap the benefits automagically
|
|
|
+as long as mtTkinter is imported at some point before extra threads are
|
|
|
+created.
|
|
|
+
|
|
|
+Author: Allen B. Taylor, a.b.taylor@gmail.com
|
|
|
+'''
|
|
|
+
|
|
|
+from Tkinter import *
|
|
|
+import threading
|
|
|
+import Queue
|
|
|
+
|
|
|
+class _Tk(object):
|
|
|
+ """
|
|
|
+ Wrapper for underlying attribute tk of class Tk.
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self, tk, mtDebug = 0, mtCheckPeriod = 10):
|
|
|
+ self._tk = tk
|
|
|
+
|
|
|
+ # Create the incoming event queue.
|
|
|
+ self._eventQueue = Queue.Queue(1)
|
|
|
+
|
|
|
+ # Identify the thread from which this object is being created so we can
|
|
|
+ # tell later whether an event is coming from another thread.
|
|
|
+ self._creationThread = threading.currentThread()
|
|
|
+
|
|
|
+ # Store remaining values.
|
|
|
+ self._debug = mtDebug
|
|
|
+ self._checkPeriod = mtCheckPeriod
|
|
|
+ self._destroying = False
|
|
|
+
|
|
|
+ def __getattr__(self, name):
|
|
|
+ # Divert attribute accesses to a wrapper around the underlying tk
|
|
|
+ # object.
|
|
|
+ return _TkAttr(self, getattr(self._tk, name))
|
|
|
+
|
|
|
+class _TkAttr(object):
|
|
|
+ """
|
|
|
+ Thread-safe callable attribute wrapper.
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(self, tk, attr):
|
|
|
+ self._tk = tk
|
|
|
+ self._attr = attr
|
|
|
+
|
|
|
+ def __call__(self, *args, **kwargs):
|
|
|
+ """
|
|
|
+ Thread-safe method invocation.
|
|
|
+ Diverts out-of-thread calls through the event queue.
|
|
|
+ Forwards all other method calls to the underlying tk object directly.
|
|
|
+ """
|
|
|
+
|
|
|
+ # Check if we're in the creation thread.
|
|
|
+ if threading.currentThread() == self._tk._creationThread:
|
|
|
+ # We're in the creation thread; just call the event directly.
|
|
|
+ if self._tk._debug >= 8 or \
|
|
|
+ self._tk._debug >= 3 and self._attr.__name__ == 'call' and \
|
|
|
+ len(args) >= 1 and args[0] == 'after':
|
|
|
+ print 'Calling event directly:', \
|
|
|
+ self._attr.__name__, args, kwargs
|
|
|
+ return self._attr(*args, **kwargs)
|
|
|
+ else:
|
|
|
+ if not self._tk._destroying:
|
|
|
+ # We're in a different thread than the creation thread; enqueue
|
|
|
+ # the event, and then wait for the response.
|
|
|
+ responseQueue = Queue.Queue(1)
|
|
|
+ if self._tk._debug >= 1:
|
|
|
+ print 'Marshalling event:', self._attr.__name__, args, kwargs
|
|
|
+ self._tk._eventQueue.put((self._attr, args, kwargs, responseQueue), True, 1)
|
|
|
+ isException, response = responseQueue.get(True, 1)
|
|
|
+
|
|
|
+ # Handle the response, whether it's a normal return value or
|
|
|
+ # an exception.
|
|
|
+ if isException:
|
|
|
+ exType, exValue, exTb = response
|
|
|
+ raise exType, exValue, exTb
|
|
|
+ else:
|
|
|
+ return response
|
|
|
+
|
|
|
+# Define a hook for class Tk's __init__ method.
|
|
|
+def _Tk__init__(self, *args, **kwargs):
|
|
|
+ # We support some new keyword arguments that the original __init__ method
|
|
|
+ # doesn't expect, so separate those out before doing anything else.
|
|
|
+ new_kwnames = ('mtCheckPeriod', 'mtDebug')
|
|
|
+ new_kwargs = {}
|
|
|
+ for name, value in kwargs.items():
|
|
|
+ if name in new_kwnames:
|
|
|
+ new_kwargs[name] = value
|
|
|
+ del kwargs[name]
|
|
|
+
|
|
|
+ # Call the original __init__ method, creating the internal tk member.
|
|
|
+ self.__original__init__mtTkinter(*args, **kwargs)
|
|
|
+
|
|
|
+ # Replace the internal tk member with a wrapper that handles calls from
|
|
|
+ # other threads.
|
|
|
+ self.tk = _Tk(self.tk, **new_kwargs)
|
|
|
+
|
|
|
+ # Set up the first event to check for out-of-thread events.
|
|
|
+ self.after_idle(_CheckEvents, self)
|
|
|
+
|
|
|
+# Define a hook for class Tk's destroy method.
|
|
|
+def _Tk_destroy(self):
|
|
|
+ self.tk._destroying = True
|
|
|
+ self.__original__destroy()
|
|
|
+
|
|
|
+# Replace Tk's original __init__ with the hook.
|
|
|
+Tk.__original__init__mtTkinter = Tk.__init__
|
|
|
+Tk.__init__ = _Tk__init__
|
|
|
+
|
|
|
+# Replace Tk's original destroy with the hook.
|
|
|
+Tk.__original__destroy = Tk.destroy
|
|
|
+Tk.destroy = _Tk_destroy
|
|
|
+
|
|
|
+def _CheckEvents(tk):
|
|
|
+ "Event checker event."
|
|
|
+
|
|
|
+ used = False
|
|
|
+ try:
|
|
|
+ # Process all enqueued events, then exit.
|
|
|
+ while True:
|
|
|
+ try:
|
|
|
+ # Get an event request from the queue.
|
|
|
+ method, args, kwargs, responseQueue = \
|
|
|
+ tk.tk._eventQueue.get_nowait()
|
|
|
+ except:
|
|
|
+ # No more events to process.
|
|
|
+ break
|
|
|
+ else:
|
|
|
+ # Call the event with the given arguments, and then return
|
|
|
+ # the result back to the caller via the response queue.
|
|
|
+ used = True
|
|
|
+ if tk.tk._debug >= 2:
|
|
|
+ print 'Calling event from main thread:', \
|
|
|
+ method.__name__, args, kwargs
|
|
|
+ try:
|
|
|
+ responseQueue.put((False, method(*args, **kwargs)))
|
|
|
+ except SystemExit, ex:
|
|
|
+ raise SystemExit, ex
|
|
|
+ except Exception, ex:
|
|
|
+ # Calling the event caused an exception; return the
|
|
|
+ # exception back to the caller so that it can be raised
|
|
|
+ # in the caller's thread.
|
|
|
+ from sys import exc_info
|
|
|
+ exType, exValue, exTb = exc_info()
|
|
|
+ responseQueue.put((True, (exType, exValue, exTb)))
|
|
|
+ finally:
|
|
|
+ # Schedule to check again. If we just processed an event, check
|
|
|
+ # immediately; if we didn't, check later.
|
|
|
+ if used:
|
|
|
+ tk.after_idle(_CheckEvents, tk)
|
|
|
+ else:
|
|
|
+ tk.after(tk.tk._checkPeriod, _CheckEvents, tk)
|
|
|
+
|
|
|
+# Test thread entry point.
|
|
|
+def _testThread(root):
|
|
|
+ text = "This is Tcl/Tk version %s" % TclVersion
|
|
|
+ if TclVersion >= 8.1:
|
|
|
+ try:
|
|
|
+ text = text + unicode("\nThis should be a cedilla: \347",
|
|
|
+ "iso-8859-1")
|
|
|
+ except NameError:
|
|
|
+ pass # no unicode support
|
|
|
+ try:
|
|
|
+ if root.globalgetvar('tcl_platform(threaded)'):
|
|
|
+ text = text + "\nTcl is built with thread support"
|
|
|
+ else:
|
|
|
+ raise RuntimeError
|
|
|
+ except:
|
|
|
+ text = text + "\nTcl is NOT built with thread support"
|
|
|
+ text = text + "\nmtTkinter works with or without Tcl thread support"
|
|
|
+ label = Label(root, text=text)
|
|
|
+ label.pack()
|
|
|
+ button = Button(root, text="Click me!",
|
|
|
+ command=lambda root=root: root.button.configure(
|
|
|
+ text="[%s]" % root.button['text']))
|
|
|
+ button.pack()
|
|
|
+ root.button = button
|
|
|
+ quit = Button(root, text="QUIT", command=root.destroy)
|
|
|
+ quit.pack()
|
|
|
+ # The following three commands are needed so the window pops
|
|
|
+ # up on top on Windows...
|
|
|
+ root.iconify()
|
|
|
+ root.update()
|
|
|
+ root.deiconify()
|
|
|
+ # Simulate button presses...
|
|
|
+ button.invoke()
|
|
|
+ root.after(1000, _pressOk, root, button)
|
|
|
+
|
|
|
+# Test button continuous press event.
|
|
|
+def _pressOk(root, button):
|
|
|
+ button.invoke()
|
|
|
+ try:
|
|
|
+ root.after(1000, _pressOk, root, button)
|
|
|
+ except:
|
|
|
+ pass # Likely we're exiting
|
|
|
+
|
|
|
+# Test. Mostly borrowed from the Tkinter module, but the important bits moved
|
|
|
+# into a separate thread.
|
|
|
+if __name__ == '__main__':
|
|
|
+ import threading
|
|
|
+ root = Tk(mtDebug = 1)
|
|
|
+ thread = threading.Thread(target = _testThread, args=(root,))
|
|
|
+ thread.start()
|
|
|
+ root.mainloop()
|
|
|
+ thread.join()
|