【问题标题】:Correct scrolling/resizing behavior of FigureCanvasTkAgg canvas widget when the contained figure is resized调整包含的图形大小时,纠正 FigureCanvasTkAgg 画布小部件的滚动/调整大小行为
【发布时间】:2021-01-09 22:32:00
【问题描述】:

我正在尝试使用 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()

【问题讨论】:

    标签: python matplotlib tkinter


    【解决方案1】:

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

    【讨论】:

    • 我想我明白为什么这种行为可能有用了。我认为这种解决方法不适用于我的目的,因为我希望用户能够独立于以英寸为单位的图形大小设置图形 DPI,因为我希望他们能够对导出后图形的外观有一个所见即所得的视图到图像文件
    • 您的回答确实促使我深入研究源代码,似乎我可以通过生成具有我想要的宽度和高度的虚假 tkinter 事件并将其传递给 @987654330 来伪造调整大小的行为@函数。
    【解决方案2】:

    感谢 j_4321 的回答,在深入研究 FigureCanvasTk 对象的源代码后,我想出了以下解决方案,它可以满足我的一切需求。

    看起来canvas对象的作用是在每次调整canvas小部件大小时使用tkinter.Canvas.create_image生成一个新的图形图像,保持图形的DPI不变并设置图形的宽度和高度保持它与画布小部件的大小相同。由于我希望画布小部件调整为图形的大小,因此我从图形属性计算宽度和高度,然后将自定义事件对象传递给 FigureCanvasTk.resize,这是画布小部件的 &lt;Configure&gt; 事件的回调函数。

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

    这是完整的示例代码:

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

    【讨论】:

      猜你喜欢
      • 2018-03-01
      • 1970-01-01
      • 2021-04-06
      • 2014-06-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-01-14
      • 1970-01-01
      相关资源
      最近更新 更多