调整包含的图形大小时,FigureCastKagg画布小部件的正确滚动/调整大小行为

2024-05-01 03:15:15 发布

您现在位置:Python中文网/ 问答频道 /正文

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

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

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()

Tags: fromimportself图形matplotlibcolumntkgrid
2条回答

多亏了j_4321的回答,在深入研究了FigureCanvasTk对象的源代码之后,我想出了下面的解决方案,它完成了我所需要的一切

看起来canvas对象所做的就是在每次调整canvas小部件的大小时使用tkinter.Canvas.create_image生成图形的新图像,保持图形的DPI不变,并设置图形的宽度和高度以保持其与canvas小部件相同的大小。由于我希望画布小部件根据图形大小调整大小,因此我从图形属性计算宽度和高度,然后将自定义事件对象传递给FigureCanvasTk.resize,即画布小部件的<Configure>事件的回调函数

最后一个技巧是将画布的滚动区域设置为仅创建的最后一个画布项的大小。看起来没有从画布中删除图形的先前迭代(似乎是内存泄漏?),因此,如果尝试将scrollregion设置为Canvas.bbox('all'),它会将scrollregion设置为图形最大版本的大小,而不是图形的当前版本

以下是完整的示例代码:

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)
        buttonframe = tk.Frame(self)
        self._sizebutton = tk.Button(buttonframe,text="Size (in.)", command=self._change_size)
        self._dpibutton = tk.Button(buttonframe,text="DPI", command=self._change_dpi)
        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>",self._refresh)

        self._sizebutton.grid(row=0,column=0,sticky='w')
        self._dpibutton.grid(row=0,column=1,sticky='w')
        buttonframe.grid(row=0,column=0,columnspan=2,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)

        # Refresh the canvas to show the new plot
        self._canvas.draw()
    
    # Figure size change button callback
    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._refresh()

    # Figure DPI change button callback
    def _change_dpi(self):
        newdpi = askfloat('DPI', 'Input a new DPI for the figure')
        if newdpi is None:
            return
        self._figure.set_dpi(newdpi)
        self._refresh()
    
    # Refresh function to make the figure canvas widget display the entire figure
    def _refresh(self,event=None):
        # Get the width and height of the *figure* in pixels
        w = self._figure.get_figwidth()*self._figure.get_dpi()
        h = self._figure.get_figheight()*self._figure.get_dpi()

        # Generate a blank tkinter Event object
        evnt = tk.Event()
        # Set the "width" and "height" values of the event
        evnt.width = w
        evnt.height = h

        # Set the width and height of the canvas widget
        self._cwidg.configure(width=w,height=h)
        self._cwidg.update_idletasks()

        # Pass the generated event object to the FigureCanvasTk.resize() function
        self._canvas.resize(evnt)
        # Set the scroll region to *only* the area of the last canvas item created.
        # Otherwise, the scrollregion will always be the size of the largest iteration
        # of the figure.
        self._cwidg.configure(scrollregion=self._cwidg.bbox(self._cwidg.find_all()[-1]))
        
root = tk.Tk()

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

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

root.mainloop()

这里的主要问题是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()

相关问题 更多 >