diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index d4c2ad474165..a18a9bd8660f 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -270,14 +270,24 @@ def _update_device_pixel_ratio(self, event=None): elif sys.platform == "linux": ratio = self._tkcanvas.winfo_fpixels('1i') / 96 if ratio is not None and self._set_device_pixel_ratio(ratio): - # The easiest way to resize the canvas is to resize the canvas - # widget itself, since we implement all the logic for resizing the - # canvas backing store on that event. + # Resize the canvas widget, then explicitly update the figure + # size to match the actual widget dimensions. When the canvas + # is constrained by a geometry manager (pack/grid), + # may not fire after configure(), so we handle the resize + # directly — similar to Qt's _update_pixel_ratio approach. w, h = self.get_width_height(physical=True) self._tkcanvas.configure(width=w, height=h) + self._resize_figure_for_canvas_size( + self._tkcanvas.winfo_width(), + self._tkcanvas.winfo_height()) def resize(self, event): - width, height = event.width, event.height + self._resize_figure_for_canvas_size(event.width, event.height) + + def _resize_figure_for_canvas_size(self, width, height): + """Update figure size and redraw based on a given canvas size.""" + if width <= 0 or height <= 0: + return # compute desired figure size in inches dpival = self.figure.dpi diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index 1f96ad1308cb..32212610ffc1 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -4,6 +4,7 @@ import platform import subprocess import sys +from unittest.mock import patch import pytest @@ -280,3 +281,52 @@ def test_figure(master): foreground="white") test_figure(root) print("success") + + +@_isolated_tk_test(success_count=1) +def test_dpi_change_triggers_resize(): + """ + Test that _update_device_pixel_ratio recalculates figure.size_inches + using the actual widget dimensions, so the render size matches the + visible canvas area even when constrained by a layout manager. + See issue #31126. + """ + import tkinter as tk + from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg + from matplotlib.figure import Figure + + root = tk.Tk() + root.geometry("400x300") + root.update_idletasks() + + fig = Figure(dpi=100) + fig.add_subplot(111) + + canvas = FigureCanvasTkAgg(fig, master=root) + canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + canvas.draw() + root.update_idletasks() + + actual_w = canvas.get_tk_widget().winfo_width() + actual_h = canvas.get_tk_widget().winfo_height() + assert actual_w > 0 and actual_h > 0 + + # Simulate a 2x DPI change through _update_device_pixel_ratio. + # Mock the platform-specific DPI query to return ratio=2.0. + with patch.object(sys, 'platform', 'linux'), \ + patch.object(canvas._tkcanvas, 'winfo_fpixels', + return_value=192.0): + canvas._update_device_pixel_ratio() + + # Verify the render size matches the actual widget size, NOT the + # inflated physical size from get_width_height(physical=True). + size = fig.get_size_inches() + render_w = round(size[0] * fig.dpi) + render_h = round(size[1] * fig.dpi) + assert abs(render_w - actual_w) <= 2, \ + f"render width {render_w} != actual width {actual_w}" + assert abs(render_h - actual_h) <= 2, \ + f"render height {render_h} != actual height {actual_h}" + + print("success") + root.destroy()