首页 > 解决方案 > 当调整包含的图形大小时,纠正 FigureCanvasTkAgg 画布小部件的滚动/调整大小行为

问题描述

我正在尝试使用 Tkinter 和 matplotlib(python 3.7 和 matplotlib 3.0.0)制作交互式绘图 GUI 我希望用户能够在不调整窗口大小的情况下调整屏幕上显示的图形大小,并通过编辑图形的 dpi、宽度和高度属性。到目前为止,这是可行的,但是如果图形大于显示区域,我希望用户能够滚动查看整个图形。如果图形小于显示区域,我希望禁用滚动条。

我曾尝试将滚动条直接应用到 FigureCanvasTkAgg 对象本身,以及将 FigureCanvasTkAgg 画布嵌入到第二个可滚动画布中,但似乎问题在于 FigureCanvasTkAgg 小部件的可绘制区域在图形大小发生变化时不会改变。重现问题的最小代码如下。是否有一些我缺少的 FigureCanvasTkAgg 对象的属性可以使它工作?

import tkinter as tk
from tkinter import ttk
from tkinter.simpledialog import askfloat
from matplotlib.figure import Figure
from matplotlib.axes   import Axes
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
          
class InteractivePlot(tk.Frame):
    def __init__(self,master,**kwargs):
        super().__init__(master,**kwargs)
        self._figure = Figure(dpi=150)
        self._canvas = FigureCanvasTkAgg(self._figure, master=self)
        self._sizebutton = tk.Button(self,text="Size (in.)", command=self._change_size)
        self._axis = self._figure.add_subplot(111)

        # Plot some data just to have something to look at.
        self._axis.plot([0,1,2,3,4,5],[1,1,3,3,5,5],label='Dummy Data')

        self._cwidg = self._canvas.get_tk_widget()
        self._scrx = ttk.Scrollbar(self,orient="horizontal", command=self._cwidg.xview)
        self._scry = ttk.Scrollbar(self,orient="vertical", command=self._cwidg.yview)
        self._cwidg.configure(yscrollcommand=self._scry.set, xscrollcommand=self._scrx.set)

        self._cwidg.bind(
            "<Configure>",
            lambda e: self._cwidg.configure(
                scrollregion=self._cwidg.bbox("all")
            )
        )

        self._sizebutton.grid(row=0,column=0,sticky='w')
        self._cwidg. grid(row=1,column=0,sticky='news')
        self._scrx.  grid(row=2,column=0,sticky='ew')
        self._scry.  grid(row=1,column=1,sticky='ns')

        self.rowconfigure(1,weight=1)
        self.columnconfigure(0,weight=1)

        self._canvas.draw()
    
    def _change_size(self):
        newsize = askfloat('Size','Input new size in inches')
        if newsize is None:
            return
        w = newsize
        h = newsize/1.8
        self._figure.set_figwidth(w)
        self._figure.set_figheight(h)
        self._canvas.draw()
        
root = tk.Tk()

plt = InteractivePlot(root,width=400,height=400)

plt.pack(fill=tk.BOTH,expand=True)

root.mainloop()

标签: pythonmatplotlibtkinter

解决方案


这里的主要问题是 matplotlib 图形是用来调整大小的self._cwidg。由于假定图形始终与 具有相同的大小self._cwidg,因此 matplotlib 仅重绘图形中可见的部分,而self._cwidg当图形大小发生变化时,后者不会调整大小。

一种解决方法是使用额外的画布并将其作为窗口self._scroll_canvas嵌入其中。self._cwidg然后我_change_size()通过以下方式进行了修改:

def _change_size(self):
    newsize = askfloat('Size', 'Input new size in inches')
    if newsize is None:
        return
    w = newsize
    h = newsize/1.8
    self._cwidg.configure(width=int(w*self._conv_ratio), height=int(h*self._conv_ratio))
    self._scroll_canvas.configure(scrollregion=self._scroll_canvas.bbox("all"))

我直接调整大小self._cwidg,然后调整图形的大小,确保重绘它的每个部分。然后我更新滚动区域。这是完整的代码:

import tkinter as tk
from tkinter import ttk
from tkinter.simpledialog import askfloat
from matplotlib.figure import Figure
from matplotlib.axes import Axes
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

class InteractivePlot(tk.Frame):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)

        self._scroll_canvas = tk.Canvas(self)
        self._figure = Figure(dpi=150)
        self._canvas = FigureCanvasTkAgg(self._figure, master=self._scroll_canvas)
        self._sizebutton = tk.Button(self, text="Size (in.)", command=self._change_size)
        self._axis = self._figure.add_subplot(111)

        # Plot some data just to have something to look at.
        self._axis.plot([0, 1, 2, 3, 4, 5], [1, 1, 3, 3, 5, 5], label='Dummy Data')



        self._cwidg = self._canvas.get_tk_widget()
        self._scroll_canvas.create_window(0, 0, anchor='nw', window=self._cwidg)

        self._scrx = ttk.Scrollbar(self, orient="horizontal", command=self._scroll_canvas.xview)
        self._scry = ttk.Scrollbar(self, orient="vertical", command=self._scroll_canvas.yview)
        self._scroll_canvas.configure(yscrollcommand=self._scry.set, xscrollcommand=self._scrx.set)


        self._sizebutton.grid(row=0, column=0, sticky='w')
        self._scroll_canvas.grid(row=1, column=0, sticky='news')
        self._scrx.grid(row=2, column=0, sticky='ew')
        self._scry.grid(row=1, column=1, sticky='ns')

        self.rowconfigure(1, weight=1)
        self.columnconfigure(0, weight=1)

        self._canvas.draw()

        wi = self._figure.get_figwidth()
        wp = self._cwidg.winfo_reqwidth(),
        self._conv_ratio = wp / wi  # get inch to pixel conversion factor
        self._scroll_canvas.configure(width=wp, height=self._cwidg.winfo_reqheight())
        self._scroll_canvas.configure(scrollregion=self._scroll_canvas.bbox("all"))

    def _change_size(self):
        newsize = askfloat('Size', 'Input new size in inches')
        if newsize is None:
            return
        w = newsize
        h = newsize/1.8
        self._cwidg.configure(width=int(w*self._conv_ratio), height=int(h*self._conv_ratio))
        self._scroll_canvas.configure(scrollregion=self._scroll_canvas.bbox("all"))

root = tk.Tk()
plt = InteractivePlot(root, width=400, height=400)
plt.pack(fill=tk.BOTH, expand=True)
root.mainloop()

推荐阅读