Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 97fb361

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

File tree

Expand file treeCollapse file tree

3 files changed

+96
-14
lines changed
Filter options
Expand file treeCollapse file tree

3 files changed

+96
-14
lines changed

‎lib/matplotlib/backends/_backend_tk.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/_backend_tk.py
+19-12Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/cbook/__init__.py
+2-1Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_backend_tk.py
+75-1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.