import sys import threading if sys.version_info[0] == 2: # Python 2 from Tkinter import * import Queue as queue else: # Python 3 from tkinter import * import queue class _Tk(object): """Wrapper for underlying attribute tk of class Tk""" def __init__(self, tk, mt_debug=0, mt_check_period=10): """ :param tk: Tkinter.Tk.tk Tk interpreter object :param mt_debug: Determines amount of debug output. 0 = No debug output (default) 1 = Minimal debug output ... 9 = Full debug output :param mt_check_period: Amount of time in milliseconds (default 10) 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. # TODO: Replace custom logging functionality with standard # TODO: logging.Logger for easier access and standardization """ self._tk = tk # Create the incoming event queue self._event_queue = 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._creation_thread = threading.current_thread() # Create attributes for kwargs self._debug = mt_debug self._check_period = mt_check_period # Destroying flag to be set by the .destroy() hook self._destroying = False def __getattr__(self, name): """ Diverts 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.current_thread() == self._tk._creation_thread: # 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. response_queue = queue.Queue(1) if self._tk._debug >= 1: print('Marshalling event:', self._attr.__name__, args, kwargs) self._tk._event_queue.put((self._attr, args, kwargs, response_queue), True, 1) is_exception, response = response_queue.get(True, None) # Handle the response, whether it's a normal return value or # an exception. if is_exception: ex_type, ex_value, ex_tb = response raise ex_type(ex_value, ex_tb) return response def _Tk__init__(self, *args, **kwargs): """ Hook for Tkinter.Tk.__init__ method :param self: Tk instance :param args, kwargs: Arguments for Tk initializer """ # We support some new keyword arguments that the original __init__ method # doesn't expect, so separate those out before doing anything else. new_kwnames = ('mt_check_period', 'mt_debug') new_kwargs = { kw_name: kwargs.pop(kw_name) for kw_name in new_kwnames if kwargs.get(kw_name, None) is not None } # 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(_check_events, self) # Define a hook for class Tk's destroy method. def _Tk_destroy(self): self.tk._destroying = True self.__original__destroy() def _check_events(tk): """Checks events in the queue on a given Tk instance""" used = False try: # Process all enqueued events, then exit. while True: try: # Get an event request from the queue. method, args, kwargs, response_queue = tk.tk._event_queue.get_nowait() except queue.Empty: # 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: response_queue.put((False, method(*args, **kwargs))) except SystemExit: raise # Raises original SystemExit except Exception: # 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 # Python 2 requirement ex_type, ex_value, ex_tb = exc_info() response_queue.put((True, (ex_type, ex_value, ex_tb))) finally: # Schedule to check again. If we just processed an event, check # immediately; if we didn't, check later. if used: tk.after_idle(_check_events, tk) else: tk.after(tk.tk._check_period, _check_events, tk) """Perform in-memory modification of Tkinter module""" # 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