From 11a4f8fd7e64ab8ed73d5bf4eb26c1bb793bb31b Mon Sep 17 00:00:00 2001 From: intelliking Date: Wed, 11 Feb 2026 02:04:21 +0000 Subject: [PATCH 1/7] fix: resolve FigureCanvasTkAgg clipping on Windows HiDPI --- lib/matplotlib/backends/_backend_tk.py | 11 +++++ lib/matplotlib/tests/test_backend_tk.py | 58 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index d4c2ad474165..3645993988fd 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -275,6 +275,17 @@ def _update_device_pixel_ratio(self, event=None): # canvas backing store on that event. w, h = self.get_width_height(physical=True) self._tkcanvas.configure(width=w, height=h) + + # If the canvas is constrained by a layout manager (pack/grid), + # the actual displayed size may not match the configured size. + # In this case, won't fire, so we need to explicitly + # call resize() to recalculate figure.size_inches with the new DPI. + self._tkcanvas.update_idletasks() + actual_w = self._tkcanvas.winfo_width() + actual_h = self._tkcanvas.winfo_height() + if actual_w > 0 and actual_h > 0 and (actual_w != w or actual_h != h): + # Create a mock event object with the actual dimensions + self.resize(type('Event', (), {'width': actual_w, 'height': actual_h})()) def resize(self, event): width, height = event.width, event.height diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index 1f96ad1308cb..7376001e707d 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -280,3 +280,61 @@ def test_figure(master): foreground="white") test_figure(root) print("success") + + +@_isolated_tk_test(success_count=1) +def test_hidpi_embedded_canvas(): + """ + Test that embedded canvas in layout-managed container handles HiDPI + correctly without clipping. This tests the fix for issue #31126. + """ + import tkinter as tk + from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg + from matplotlib.figure import Figure + + root = tk.Tk() + root.geometry("800x600") + + # Create a frame with pack layout manager + frame = tk.Frame(root) + frame.pack(fill=tk.BOTH, expand=True) + + # Create figure with initial DPI + fig = Figure(dpi=96) + ax = fig.add_subplot(111) + ax.plot([1, 2, 3], [1, 2, 3]) + ax.set_xlabel("X Axis") + ax.set_ylabel("Y Axis") + ax.set_title("HiDPI Test") + + # Embed canvas in the frame + canvas = FigureCanvasTkAgg(fig, master=frame) + canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + def check_sizes(): + # Force a draw and update + canvas.draw() + root.update_idletasks() + + # Get actual canvas size + w = canvas.get_tk_widget() + actual_w = w.winfo_width() + actual_h = w.winfo_height() + + # Get render size + sz = fig.get_size_inches() + render_w = int(sz[0] * fig.dpi) + render_h = int(sz[1] * fig.dpi) + + # The render size should match the actual size (within small tolerance + # for rounding). This verifies that figure.size_inches was properly + # recalculated after DPI change. + # Allow 2 pixel tolerance for rounding differences + if abs(render_w - actual_w) <= 2 and abs(render_h - actual_h) <= 2: + print("success") + + root.destroy() + + # Give the window time to appear and process DPI updates + root.after(500, check_sizes) + root.mainloop() From 538486b8b1ddf07b3e20a8e513159d4800dd8f0f Mon Sep 17 00:00:00 2001 From: intelliking Date: Wed, 11 Feb 2026 02:28:36 +0000 Subject: [PATCH 2/7] Fix lint issues --- lib/matplotlib/backends/_backend_tk.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 3645993988fd..ad6f593cc40e 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -275,7 +275,7 @@ def _update_device_pixel_ratio(self, event=None): # canvas backing store on that event. w, h = self.get_width_height(physical=True) self._tkcanvas.configure(width=w, height=h) - + # If the canvas is constrained by a layout manager (pack/grid), # the actual displayed size may not match the configured size. # In this case, won't fire, so we need to explicitly @@ -285,7 +285,10 @@ def _update_device_pixel_ratio(self, event=None): actual_h = self._tkcanvas.winfo_height() if actual_w > 0 and actual_h > 0 and (actual_w != w or actual_h != h): # Create a mock event object with the actual dimensions - self.resize(type('Event', (), {'width': actual_w, 'height': actual_h})()) + event = type('Event', (), { + 'width': actual_w, 'height': actual_h + })() + self.resize(event) def resize(self, event): width, height = event.width, event.height From f13bb92eb4f388760b0c87b449f24d40b9f216ea Mon Sep 17 00:00:00 2001 From: intelliking Date: Wed, 11 Feb 2026 03:09:29 +0000 Subject: [PATCH 3/7] Fix update_idletasks call in Tk backend --- lib/matplotlib/backends/_backend_tk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index ad6f593cc40e..2f0ca7ef7563 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -280,7 +280,7 @@ def _update_device_pixel_ratio(self, event=None): # the actual displayed size may not match the configured size. # In this case, won't fire, so we need to explicitly # call resize() to recalculate figure.size_inches with the new DPI. - self._tkcanvas.update_idletasks() + self._tkcanvas.master.update_idletasks() actual_w = self._tkcanvas.winfo_width() actual_h = self._tkcanvas.winfo_height() if actual_w > 0 and actual_h > 0 and (actual_w != w or actual_h != h): From 61763bd13eca60669be9c81533dccfc5ce2e9560 Mon Sep 17 00:00:00 2001 From: intelliking Date: Wed, 11 Feb 2026 12:53:47 +0000 Subject: [PATCH 4/7] Check for update_idletasks existence before calling --- lib/matplotlib/backends/_backend_tk.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 2f0ca7ef7563..c5033f25a838 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -280,15 +280,16 @@ def _update_device_pixel_ratio(self, event=None): # the actual displayed size may not match the configured size. # In this case, won't fire, so we need to explicitly # call resize() to recalculate figure.size_inches with the new DPI. - self._tkcanvas.master.update_idletasks() - actual_w = self._tkcanvas.winfo_width() - actual_h = self._tkcanvas.winfo_height() - if actual_w > 0 and actual_h > 0 and (actual_w != w or actual_h != h): - # Create a mock event object with the actual dimensions - event = type('Event', (), { - 'width': actual_w, 'height': actual_h - })() - self.resize(event) + if hasattr(tk.Misc, 'update_idletasks'): + self._tkcanvas.master.update_idletasks() + actual_w = self._tkcanvas.winfo_width() + actual_h = self._tkcanvas.winfo_height() + if actual_w > 0 and actual_h > 0 and (actual_w != w or actual_h != h): + # Create a mock event object with the actual dimensions + event = type('Event', (), { + 'width': actual_w, 'height': actual_h + })() + self.resize(event) def resize(self, event): width, height = event.width, event.height From 6eed3272392bac0313b82393cf636864eed8bd0e Mon Sep 17 00:00:00 2001 From: intelliking Date: Wed, 25 Feb 2026 14:09:58 +0000 Subject: [PATCH 5/7] refactor: fix HiDPI clipping for layout-managed FigureCanvasTkAgg by resizing with actual widget dimensions instead of computed ones after DPI changes --- lib/matplotlib/backends/_backend_tk.py | 35 +++++----- lib/matplotlib/tests/test_backend_tk.py | 89 +++++++++++-------------- 2 files changed, 56 insertions(+), 68 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index c5033f25a838..322f313d0863 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -270,29 +270,26 @@ 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. + # The easiest way to resize the canvas is to emit a + # resize event since we implement all the logic for resizing + # the canvas backing store on that event. w, h = self.get_width_height(physical=True) self._tkcanvas.configure(width=w, height=h) - - # If the canvas is constrained by a layout manager (pack/grid), - # the actual displayed size may not match the configured size. - # In this case, won't fire, so we need to explicitly - # call resize() to recalculate figure.size_inches with the new DPI. - if hasattr(tk.Misc, 'update_idletasks'): - self._tkcanvas.master.update_idletasks() - actual_w = self._tkcanvas.winfo_width() - actual_h = self._tkcanvas.winfo_height() - if actual_w > 0 and actual_h > 0 and (actual_w != w or actual_h != h): - # Create a mock event object with the actual dimensions - event = type('Event', (), { - 'width': actual_w, 'height': actual_h - })() - self.resize(event) + # Use the current actual widget size to recalculate + # figure.size_inches with the new DPI. 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. + self._resize_canvas( + self._tkcanvas.winfo_width(), + self._tkcanvas.winfo_height()) def resize(self, event): - width, height = event.width, event.height + self._resize_canvas(event.width, event.height) + + def _resize_canvas(self, width, height): + 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 7376001e707d..b5255aa29d90 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -4,10 +4,14 @@ import platform import subprocess import sys +import tkinter as tk +from unittest.mock import patch import pytest from matplotlib import _c_internal_utils +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +from matplotlib.figure import Figure from matplotlib.testing import subprocess_run_helper @@ -283,58 +287,45 @@ def test_figure(master): @_isolated_tk_test(success_count=1) -def test_hidpi_embedded_canvas(): +def test_dpi_change_triggers_resize(): """ - Test that embedded canvas in layout-managed container handles HiDPI - correctly without clipping. This tests the fix for issue #31126. + 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("800x600") - - # Create a frame with pack layout manager - frame = tk.Frame(root) - frame.pack(fill=tk.BOTH, expand=True) - - # Create figure with initial DPI - fig = Figure(dpi=96) - ax = fig.add_subplot(111) - ax.plot([1, 2, 3], [1, 2, 3]) - ax.set_xlabel("X Axis") - ax.set_ylabel("Y Axis") - ax.set_title("HiDPI Test") - - # Embed canvas in the frame - canvas = FigureCanvasTkAgg(fig, master=frame) - canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + root.geometry("400x300") + root.update_idletasks() - def check_sizes(): - # Force a draw and update - canvas.draw() - root.update_idletasks() - - # Get actual canvas size - w = canvas.get_tk_widget() - actual_w = w.winfo_width() - actual_h = w.winfo_height() - - # Get render size - sz = fig.get_size_inches() - render_w = int(sz[0] * fig.dpi) - render_h = int(sz[1] * fig.dpi) - - # The render size should match the actual size (within small tolerance - # for rounding). This verifies that figure.size_inches was properly - # recalculated after DPI change. - # Allow 2 pixel tolerance for rounding differences - if abs(render_w - actual_w) <= 2 and abs(render_h - actual_h) <= 2: - print("success") + fig = Figure(dpi=100) + fig.add_subplot(111) - root.destroy() + 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}" - # Give the window time to appear and process DPI updates - root.after(500, check_sizes) - root.mainloop() + print("success") + root.destroy() From 2c0bf43a9cce351ed82e0d6a869d52bea7010315 Mon Sep 17 00:00:00 2001 From: intelliking Date: Wed, 25 Feb 2026 15:28:23 +0000 Subject: [PATCH 6/7] fix: matplot error --- lib/matplotlib/tests/test_backend_tk.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index b5255aa29d90..32212610ffc1 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -4,14 +4,11 @@ import platform import subprocess import sys -import tkinter as tk from unittest.mock import patch import pytest from matplotlib import _c_internal_utils -from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg -from matplotlib.figure import Figure from matplotlib.testing import subprocess_run_helper @@ -294,6 +291,10 @@ def test_dpi_change_triggers_resize(): 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() From 65cabc93b760a724e544c5aad2ecb567edf4fbef Mon Sep 17 00:00:00 2001 From: statxc <181730535+statxc@users.noreply.github.com> Date: Wed, 11 Mar 2026 19:35:16 +0000 Subject: [PATCH 7/7] update docstring --- lib/matplotlib/backends/_backend_tk.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 322f313d0863..a18a9bd8660f 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -270,24 +270,22 @@ 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 emit a - # resize event since we implement all the logic for resizing - # the canvas backing store on that event. - w, h = self.get_width_height(physical=True) - self._tkcanvas.configure(width=w, height=h) - # Use the current actual widget size to recalculate - # figure.size_inches with the new DPI. When the canvas is - # constrained by a geometry manager (pack/grid), + # 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. - self._resize_canvas( + 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): - self._resize_canvas(event.width, event.height) + self._resize_figure_for_canvas_size(event.width, event.height) - def _resize_canvas(self, width, 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