Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 97fb361

Browse files
authored
Merge pull request #17802 from richardsheridan/tk_backend_figuremanager_quit
fix FigureManagerTk close behavior if embedded in Tk App
2 parents d38e443 + c64c6a7 commit 97fb361

File tree

3 files changed

+96
-14
lines changed

3 files changed

+96
-14
lines changed

lib/matplotlib/backends/_backend_tk.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,8 @@ class FigureManagerTk(FigureManagerBase):
398398
The tk.Window
399399
"""
400400

401+
_owns_mainloop = False
402+
401403
def __init__(self, canvas, num, window):
402404
FigureManagerBase.__init__(self, canvas, num)
403405
self.window = window
@@ -442,9 +444,8 @@ def show(self):
442444
with _restore_foreground_window_at_end():
443445
if not self._shown:
444446
def destroy(*args):
445-
self.window = None
446447
Gcf.destroy(self)
447-
self.canvas._tkcanvas.bind("<Destroy>", destroy)
448+
self.window.protocol("WM_DELETE_WINDOW", destroy)
448449
self.window.deiconify()
449450
else:
450451
self.canvas.draw_idle()
@@ -454,15 +455,13 @@ def destroy(*args):
454455
self._shown = True
455456

456457
def destroy(self, *args):
457-
if self.window is not None:
458-
#self.toolbar.destroy()
459-
if self.canvas._idle_callback:
460-
self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback)
461-
self.window.destroy()
462-
if Gcf.get_num_fig_managers() == 0:
463-
if self.window is not None:
464-
self.window.quit()
465-
self.window = None
458+
if self.canvas._idle_callback:
459+
self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback)
460+
461+
self.window.destroy()
462+
463+
if self._owns_mainloop and not Gcf.get_num_fig_managers():
464+
self.window.quit()
466465

467466
def get_window_title(self):
468467
return self.window.wm_title()
@@ -890,4 +889,12 @@ def trigger_manager_draw(manager):
890889
def mainloop():
891890
managers = Gcf.get_all_fig_managers()
892891
if managers:
893-
managers[0].window.mainloop()
892+
first_manager = managers[0]
893+
manager_class = type(first_manager)
894+
if manager_class._owns_mainloop:
895+
return
896+
manager_class._owns_mainloop = True
897+
try:
898+
first_manager.window.mainloop()
899+
finally:
900+
manager_class._owns_mainloop = False

lib/matplotlib/cbook/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,10 @@ def _get_running_interactive_framework():
6464
return "wx"
6565
tkinter = sys.modules.get("tkinter")
6666
if tkinter:
67+
codes = {tkinter.mainloop.__code__, tkinter.Misc.mainloop.__code__}
6768
for frame in sys._current_frames().values():
6869
while frame:
69-
if frame.f_code == tkinter.mainloop.__code__:
70+
if frame.f_code in codes:
7071
return "tk"
7172
frame = frame.f_back
7273
if 'matplotlib.backends._macosx' in sys.modules:

lib/matplotlib/tests/test_backend_tk.py

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1-
import pytest
1+
import os
2+
import subprocess
3+
import sys
4+
import tkinter
5+
26
import numpy as np
7+
import pytest
8+
39
from matplotlib import pyplot as plt
410

511

@@ -26,3 +32,71 @@ def evil_blit(photoimage, aggimage, offsets, bboxptr):
2632
np.ones((4, 4, 4)),
2733
(0, 1, 2, 3),
2834
bad_boxes)
35+
36+
37+
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
38+
def test_figuremanager_preserves_host_mainloop():
39+
success = False
40+
41+
def do_plot():
42+
plt.figure()
43+
plt.plot([1, 2], [3, 5])
44+
plt.close()
45+
root.after(0, legitimate_quit)
46+
47+
def legitimate_quit():
48+
root.quit()
49+
nonlocal success
50+
success = True
51+
52+
root = tkinter.Tk()
53+
root.after(0, do_plot)
54+
root.mainloop()
55+
56+
assert success
57+
58+
59+
@pytest.mark.backend('TkAgg', skip_on_importerror=True)
60+
@pytest.mark.flaky(reruns=3)
61+
def test_figuremanager_cleans_own_mainloop():
62+
script = '''
63+
import tkinter
64+
import time
65+
import matplotlib.pyplot as plt
66+
import threading
67+
from matplotlib.cbook import _get_running_interactive_framework
68+
69+
root = tkinter.Tk()
70+
plt.plot([1, 2, 3], [1, 2, 5])
71+
72+
def target():
73+
while not 'tk' == _get_running_interactive_framework():
74+
time.sleep(.01)
75+
plt.close()
76+
if show_finished_event.wait():
77+
print('success')
78+
79+
show_finished_event = threading.Event()
80+
thread = threading.Thread(target=target, daemon=True)
81+
thread.start()
82+
plt.show(block=True) # testing if this function hangs
83+
show_finished_event.set()
84+
thread.join()
85+
86+
'''
87+
try:
88+
proc = subprocess.run(
89+
[sys.executable, "-c", script],
90+
env={**os.environ,
91+
"MPLBACKEND": "TkAgg",
92+
"SOURCE_DATE_EPOCH": "0"},
93+
timeout=10,
94+
stdout=subprocess.PIPE,
95+
universal_newlines=True,
96+
check=True
97+
)
98+
except subprocess.TimeoutExpired:
99+
pytest.fail("Most likely plot.show(block=True) hung")
100+
except subprocess.CalledProcessError:
101+
pytest.fail("Subprocess failed to test intended behavior")
102+
assert proc.stdout.count("success") == 1

0 commit comments

Comments
 (0)