1+ import uuid
12from contextlib import contextmanager
23import logging
34import math
@@ -44,6 +45,28 @@ def _restore_foreground_window_at_end():
4445 _c_internal_utils .Win32_SetForegroundWindow (foreground )
4546
4647
48+ _blit_args = {}
49+ # Initialize to a non-empty string that is not a Tcl command
50+ _blit_tcl_name = "mpl_blit_" + uuid .uuid4 ().hex
51+
52+
53+ def _blit (argsid ):
54+ """
55+ Thin wrapper to blit called via tkapp.call.
56+
57+ *argsid* is a unique string identifier to fetch the correct arguments from
58+ the ``_blit_args`` dict, since arguments cannot be passed directly.
59+
60+ photoimage blanking must occur in the same event and thread as blitting
61+ to avoid flickering.
62+ """
63+ photoimage , dataptr , offsets , bboxptr , blank = _blit_args .pop (argsid )
64+ if blank :
65+ photoimage .blank ()
66+ _tkagg .blit (
67+ photoimage .tk .interpaddr (), str (photoimage ), dataptr , offsets , bboxptr )
68+
69+
4770def blit (photoimage , aggimage , offsets , bbox = None ):
4871 """
4972 Blit *aggimage* to *photoimage*.
@@ -53,7 +76,10 @@ def blit(photoimage, aggimage, offsets, bbox=None):
5376 (2, 1, 0, 3) for little-endian ARBG32 (i.e. GBRA8888) data and (1, 2, 3, 0)
5477 for big-endian ARGB32 (i.e. ARGB8888) data.
5578
56- If *bbox* is passed, it defines the region that gets blitted.
79+ If *bbox* is passed, it defines the region that gets blitted. That region
80+ will NOT be blanked before blitting.
81+
82+ Tcl events must be dispatched to trigger a blit from a non-Tcl thread.
5783 """
5884 data = np .asarray (aggimage )
5985 height , width = data .shape [:2 ]
@@ -65,11 +91,31 @@ def blit(photoimage, aggimage, offsets, bbox=None):
6591 y1 = max (math .floor (y1 ), 0 )
6692 y2 = min (math .ceil (y2 ), height )
6793 bboxptr = (x1 , x2 , y1 , y2 )
94+ blank = False
6895 else :
69- photoimage .blank ()
7096 bboxptr = (0 , width , 0 , height )
71- _tkagg .blit (
72- photoimage .tk .interpaddr (), str (photoimage ), dataptr , offsets , bboxptr )
97+ blank = True
98+
99+ # NOTE: _tkagg.blit is thread unsafe and will crash the process if called
100+ # from a thread (GH#13293). Instead of blanking and blitting here,
101+ # use tkapp.call to post a cross-thread event if this function is called
102+ # from a non-Tcl thread.
103+
104+ # tkapp.call coerces all arguments to strings, so to avoid string parsing
105+ # within _blit, pack up the arguments into a global data structure.
106+ args = photoimage , dataptr , offsets , bboxptr , blank
107+ # Need a unique key to avoid thread races.
108+ # Again, make the key a string to avoid string parsing in _blit.
109+ argsid = str (id (args ))
110+ _blit_args [argsid ] = args
111+
112+ try :
113+ photoimage .tk .call (_blit_tcl_name , argsid )
114+ except tk .TclError as e :
115+ if "invalid command name" not in str (e ):
116+ raise
117+ photoimage .tk .createcommand (_blit_tcl_name , _blit )
118+ photoimage .tk .call (_blit_tcl_name , argsid )
73119
74120
75121class TimerTk (TimerBase ):
@@ -402,10 +448,18 @@ def destroy(self, *args):
402448 if self .canvas ._idle_callback :
403449 self .canvas ._tkcanvas .after_cancel (self .canvas ._idle_callback )
404450
405- self .window .destroy ()
451+ # NOTE: events need to be flushed before issuing destroy (GH #9956),
452+ # however, self.window.update() can break user code. This is the
453+ # safest way to achieve a complete draining of the event queue,
454+ # but it may require users to update() on their own to execute the
455+ # completion in obscure corner cases.
456+ def delayed_destroy ():
457+ self .window .destroy ()
458+
459+ if self ._owns_mainloop and not Gcf .get_num_fig_managers ():
460+ self .window .quit ()
406461
407- if self ._owns_mainloop and not Gcf .get_num_fig_managers ():
408- self .window .quit ()
462+ self .window .after_idle (delayed_destroy )
409463
410464 def get_window_title (self ):
411465 return self .window .wm_title ()
0 commit comments