【问题标题】:How to get a Matplotlib figure to scroll + resize properly in a Tkinter GUI如何让 Matplotlib 图形在 Tkinter GUI 中正确滚动+调整大小
【发布时间】:2012-10-23 05:51:40
【问题描述】:

我有一个 Tkinter GUI,它显示了一个 Matplotlib 图(Python 2.7.3 和 Matplotlib 1.2.0rc2)并允许用户配置图的某些方面。绘图往往会变大,因此该图形被包裹在滚动画布中。配置绘图的一个方面是更改其大小。

现在,一方面情节可以正常滚动,另一方面调整大小也可以正常工作,但这两个操作不能结合使用。下面是一个演示效果的脚本。 (对不起,我不能让它更短。)您可以滚动绘图(使用滚动条),它可以变得越来越小(使用按钮)。但是,无论何时您滚动,图形都会重置为其原始大小。显然,我希望图形的大小不会因使用滚动条而改变。

import math
from Tkinter import Tk, Button, Frame, Canvas, Scrollbar
import Tkconstants
from matplotlib import pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

def addScrollingFigure(figure, frame):

    # set up a canvas with scrollbars
    canvas = Canvas(frame)
    canvas.grid(row=0, column=0, sticky=Tkconstants.NSEW)

    xScrollbar = Scrollbar(frame, orient=Tkconstants.HORIZONTAL)
    yScrollbar = Scrollbar(frame)

    xScrollbar.grid(row=1, column=0, sticky=Tkconstants.EW)
    yScrollbar.grid(row=0, column=1, sticky=Tkconstants.NS)

    canvas.config(xscrollcommand=xScrollbar.set)
    xScrollbar.config(command=canvas.xview)
    canvas.config(yscrollcommand=yScrollbar.set)
    yScrollbar.config(command=canvas.yview)

    # plug in the figure
    figAgg = FigureCanvasTkAgg(figure, canvas)
    mplCanvas = figAgg.get_tk_widget()
    mplCanvas.grid(sticky=Tkconstants.NSEW)

    # and connect figure with scrolling region
    canvas.create_window(0, 0, window=mplCanvas)
    canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL))

def changeSize(figure, factor):
    oldSize = figure.get_size_inches()
    print "old size is", oldSize
    figure.set_size_inches([factor * s for s in oldSize])
    print "new size is", figure.get_size_inches()
    print
    figure.canvas.draw()

if __name__ == "__main__":

    root = Tk()
    root.rowconfigure(0, weight=1)
    root.columnconfigure(0, weight=1)

    frame = Frame(root)
    frame.grid(column=0, row=0, sticky=Tkconstants.NSEW)
    frame.rowconfigure(0, weight=1)
    frame.columnconfigure(0, weight=1)

    figure = plt.figure(dpi=150, figsize=(4, 4))
    plt.plot(xrange(10), [math.sin(x) for x in xrange(10)])

    addScrollingFigure(figure, frame)

    buttonFrame = Frame(root)
    buttonFrame.grid(row=0, column=1, sticky=Tkconstants.NS)
    biggerButton = Button(buttonFrame, text="larger",
                          command=lambda : changeSize(figure, 1.5))
    biggerButton.grid(column=0, row=0)
    smallerButton = Button(buttonFrame, text="smaller",
                           command=lambda : changeSize(figure, .5))
    smallerButton.grid(column=0, row=1)

    root.mainloop()

我想我错过了一些关于情节和滚动画布如何联系在一起的东西;我尝试在每次changeSize 调用后重新配置滚动画布(使用canvas.create_window(...)canvas.config(...)),但这没有帮助。我开始工作的另一种方法是在每次调整大小后重新生成整个设置(图形、画布、滚动条)。 (但是,除了看起来有点残酷之外,还有一个问题是我无法正确处理旧数据,使得程序随着时间的推移积累了相当多的内存。)

那么,有没有人知道如何让这些滚动条在调整大小操作后正常运行?

【问题讨论】:

    标签: python matplotlib tkinter


    【解决方案1】:

    对;在this answer 的滚动条讨论之后,我最终经历了这个:

    .. 我想我设法得到了一种缩放代码,它也缩放(有点)标签和填充,所以(大约)整个情节适合里面(注意,第二张图片使用来自 imgur 的“中等”比例) :

    对于非常小的尺寸,标签再次开始消失 - 但它仍然适用于一系列尺寸。

    请注意,对于较新的 matplotlib (>= 1.1.1),有一个函数 figure.tight_layout() 可以为这种情况(它是单个子图)执行边距(但不是字体大小) - 但如果您使用的是较旧的matplotlib,您可以使用figure.subplots_adjust(left=0.2, bottom=0.15, top=0.86),这就是本示例所做的;并且已经过测试:

    $ python2.7 -c 'import matplotlib; print(matplotlib.__version__)'
    0.99.3
    $ python3.2 -c 'import matplotlib; print(matplotlib.__version__)'
    1.2.0
    

    (我确实尝试查看是否可以为旧的 matplotlib 复制 tight_layout - 不幸的是,它需要包含在 tight_layout.py 中的一组相当复杂的函数,这反过来又要求 Figure 和 Axes 也具有特定的规范,不存在于 v.0.99)

    由于subplots_adjust 采用相对参数(从 0.0 到 1.0),原则上我们可以只设置一次 - 并希望它们适用于我们所需的比例范围。其余的(字体和标签板的缩放)见下面的代码:

    import math
    import sys
    if sys.version_info[0] < 3:
      from Tkinter import Tk, Button, Frame, Canvas, Scrollbar
      import Tkconstants
    else:
      from tkinter import Tk, Button, Frame, Canvas, Scrollbar
      import tkinter.constants as Tkconstants
    
    import matplotlib
    from matplotlib import pyplot as plt
    from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
    import pprint, inspect
    
    frame = None
    canvas = None
    ax = None
    
    def printBboxes(label=""):
      global canvas, mplCanvas, interior, interior_id, cwid, figure
      print("  "+label,
        "canvas.bbox:", canvas.bbox(Tkconstants.ALL),
        "mplCanvas.bbox:", mplCanvas.bbox(Tkconstants.ALL),
        "subplotpars:", figure.subplotpars.__dict__ )
    
    def addScrollingFigure(figure, frame):
      global canvas, mplCanvas, interior, interior_id, cwid
      # set up a canvas with scrollbars
      canvas = Canvas(frame)
      canvas.grid(row=1, column=1, sticky=Tkconstants.NSEW)
    
      xScrollbar = Scrollbar(frame, orient=Tkconstants.HORIZONTAL)
      yScrollbar = Scrollbar(frame)
    
      xScrollbar.grid(row=2, column=1, sticky=Tkconstants.EW)
      yScrollbar.grid(row=1, column=2, sticky=Tkconstants.NS)
    
      canvas.config(xscrollcommand=xScrollbar.set)
      xScrollbar.config(command=canvas.xview)
      canvas.config(yscrollcommand=yScrollbar.set)
      yScrollbar.config(command=canvas.yview)
    
      # plug in the figure
      figAgg = FigureCanvasTkAgg(figure, canvas)
      mplCanvas = figAgg.get_tk_widget()
    
      # and connect figure with scrolling region
      cwid = canvas.create_window(0, 0, window=mplCanvas, anchor=Tkconstants.NW)
      printBboxes("Init")
      changeSize(figure, 1)
    
    def changeSize(figure, factor):
      global canvas, mplCanvas, interior, interior_id, frame, cwid
      oldSize = figure.get_size_inches()
      print("old size is", oldSize)
      figure.set_size_inches([factor * s for s in oldSize])
      wi,hi = [i*figure.dpi for i in figure.get_size_inches()]
      print("new size is", figure.get_size_inches())
      print("new size pixels: ", wi,hi)
      mplCanvas.config(width=wi, height=hi) ; printBboxes("A")
      canvas.itemconfigure(cwid, width=wi, height=hi) ; printBboxes("B")
      canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL),width=200,height=200)
      tz.set_fontsize(tz.get_fontsize()*factor)
      for item in ([ax.title, ax.xaxis.label, ax.yaxis.label] +
                   ax.get_xticklabels() + ax.get_yticklabels()):
        item.set_fontsize(item.get_fontsize()*factor)
      ax.xaxis.labelpad = ax.xaxis.labelpad*factor
      ax.yaxis.labelpad = ax.yaxis.labelpad*factor
      #figure.tight_layout() # matplotlib > 1.1.1
      figure.subplots_adjust(left=0.2, bottom=0.15, top=0.86)
      figure.canvas.draw() ; printBboxes("C")
      print()
    
    if __name__ == "__main__":
      global root, figure
      root = Tk()
      root.rowconfigure(1, weight=1)
      root.columnconfigure(1, weight=1)
    
      frame = Frame(root)
      frame.grid(column=1, row=1, sticky=Tkconstants.NSEW)
      frame.rowconfigure(1, weight=1)
      frame.columnconfigure(1, weight=1)
    
      figure = plt.figure(dpi=150, figsize=(4, 4))
      ax = figure.add_subplot(111)
      ax.plot(range(10), [math.sin(x) for x in range(10)])
      #tz = figure.text(0.5,0.975,'The master title',horizontalalignment='center', verticalalignment='top')
      tz = figure.suptitle('The master title')
    
      ax.set_title('Tk embedding')
      ax.set_xlabel('X axis label')
      ax.set_ylabel('Y label')
      print(tz.get_fontsize()) # 12.0
      print(ax.title.get_fontsize(), ax.xaxis.label.get_fontsize(), ax.yaxis.label.get_fontsize()) # 14.4 12.0 12.0
    
      addScrollingFigure(figure, frame)
    
      buttonFrame = Frame(root)
      buttonFrame.grid(row=1, column=2, sticky=Tkconstants.NS)
      biggerButton = Button(buttonFrame, text="larger",
                            command=lambda : changeSize(figure, 1.2))
      biggerButton.grid(column=1, row=1)
      smallerButton = Button(buttonFrame, text="smaller",
                             command=lambda : changeSize(figure, 0.833))
      smallerButton.grid(column=1, row=2)
      qButton = Button(buttonFrame, text="quit",
                             command=lambda :  sys.exit(0))
      qButton.grid(column=1, row=3)
    
      root.mainloop()
    

    【讨论】:

      【解决方案2】:

      我刚刚遇到了同样的问题 - 据我所知(通过实验),除了 figure.set_size_inches(),您还必须设置 mplCanvas 的新大小以及为它创建的画布窗口的新大小,之前做figure.canvas.draw()(这也迫使一个人使用全局变量 - 或类定义)。此外,显然不需要“网格”mplCanvas - 因为它已经是 canvas 的子节点,它已经是“网格”了。并且可能想要锚定 NW,因此在每次调整大小时,绘图都会在左上角的 0,0 处重新绘制。

      这对我有用(我也尝试使用 Python Tkinter scrollbar for frame 中的“内部”框架,但没有成功;其中一些留在了 sn-p 的末尾):

      import math
      import sys
      if sys.version_info[0] < 3:
        from Tkinter import Tk, Button, Frame, Canvas, Scrollbar
        import Tkconstants
      else:
        from tkinter import Tk, Button, Frame, Canvas, Scrollbar
        import tkinter.constants as Tkconstants
      
      from matplotlib import pyplot as plt
      from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
      import pprint
      
      frame = None
      canvas = None
      
      def printBboxes(label=""):
        global canvas, mplCanvas, interior, interior_id, cwid
        print("  "+label,
          "canvas.bbox:", canvas.bbox(Tkconstants.ALL),
          "mplCanvas.bbox:", mplCanvas.bbox(Tkconstants.ALL))
      
      def addScrollingFigure(figure, frame):
        global canvas, mplCanvas, interior, interior_id, cwid
        # set up a canvas with scrollbars
        canvas = Canvas(frame)
        canvas.grid(row=1, column=1, sticky=Tkconstants.NSEW)
      
        xScrollbar = Scrollbar(frame, orient=Tkconstants.HORIZONTAL)
        yScrollbar = Scrollbar(frame)
      
        xScrollbar.grid(row=2, column=1, sticky=Tkconstants.EW)
        yScrollbar.grid(row=1, column=2, sticky=Tkconstants.NS)
      
        canvas.config(xscrollcommand=xScrollbar.set)
        xScrollbar.config(command=canvas.xview)
        canvas.config(yscrollcommand=yScrollbar.set)
        yScrollbar.config(command=canvas.yview)
      
        # plug in the figure
        figAgg = FigureCanvasTkAgg(figure, canvas)
        mplCanvas = figAgg.get_tk_widget()
        #mplCanvas.grid(sticky=Tkconstants.NSEW)
      
        # and connect figure with scrolling region
        cwid = canvas.create_window(0, 0, window=mplCanvas, anchor=Tkconstants.NW)
        printBboxes("Init")
        canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL),width=200,height=200)
      
      def changeSize(figure, factor):
        global canvas, mplCanvas, interior, interior_id, frame, cwid
        oldSize = figure.get_size_inches()
        print("old size is", oldSize)
        figure.set_size_inches([factor * s for s in oldSize])
        wi,hi = [i*figure.dpi for i in figure.get_size_inches()]
        print("new size is", figure.get_size_inches())
        print("new size pixels: ", wi,hi)
        mplCanvas.config(width=wi, height=hi) ; printBboxes("A")
        #mplCanvas.grid(sticky=Tkconstants.NSEW)
        canvas.itemconfigure(cwid, width=wi, height=hi) ; printBboxes("B")
        canvas.config(scrollregion=canvas.bbox(Tkconstants.ALL),width=200,height=200)
        figure.canvas.draw() ; printBboxes("C")
        print()
      
      if __name__ == "__main__":
        root = Tk()
        root.rowconfigure(1, weight=1)
        root.columnconfigure(1, weight=1)
      
        frame = Frame(root)
        frame.grid(column=1, row=1, sticky=Tkconstants.NSEW)
        frame.rowconfigure(1, weight=1)
        frame.columnconfigure(1, weight=1)
      
        figure = plt.figure(dpi=150, figsize=(4, 4))
        plt.plot(range(10), [math.sin(x) for x in range(10)])
      
        addScrollingFigure(figure, frame)
      
        buttonFrame = Frame(root)
        buttonFrame.grid(row=1, column=2, sticky=Tkconstants.NS)
        biggerButton = Button(buttonFrame, text="larger",
                              command=lambda : changeSize(figure, 1.5))
        biggerButton.grid(column=1, row=1)
        smallerButton = Button(buttonFrame, text="smaller",
                               command=lambda : changeSize(figure, .5))
        smallerButton.grid(column=1, row=2)
      
        root.mainloop()
      
      """
        interior = Frame(canvas) #Frame(mplCanvas) #cannot
        interior_id = canvas.create_window(0, 0, window=interior)#, anchor=Tkconstants.NW)
        canvas.config(scrollregion=canvas.bbox("all"),width=200,height=200)
        canvas.itemconfigure(interior_id, width=canvas.winfo_width())
      
        interior_id = canvas.create_window(0, 0, window=interior)#, anchor=Tkconstants.NW)
        canvas.config(scrollregion=canvas.bbox("all"),width=200,height=200)
        canvas.itemconfigure(interior_id, width=canvas.winfo_width())
      """
      

      一个有趣的注意是mplCanvas 会在它变大时遵守大小(如点击“更大”) - 但如果它变小则保持旧大小:

      $ python2.7 test.py 
      ('  Init', 'canvas.bbox:', (0, 0, 610, 610), 'mplCanvas.bbox:', (0, 0, 600, 600))
      ## here click "larger":
      ('old size is', array([ 4.06666667,  4.06666667]))
      ('new size is', array([ 6.1,  6.1]))
      ('new size pixels: ', 915.0, 915.0)
      ('  A', 'canvas.bbox:', (0, 0, 925, 925), 'mplCanvas.bbox:', (0, 0, 926, 926))
      ('  B', 'canvas.bbox:', (0, 0, 915, 915), 'mplCanvas.bbox:', (0, 0, 926, 926))
      ('  C', 'canvas.bbox:', (0, 0, 915, 915), 'mplCanvas.bbox:', (0, 0, 926, 926))
      ()
      ## here click "larger":
      ('old size is', array([ 6.1,  6.1]))
      ('new size is', array([ 9.15,  9.15]))
      ('new size pixels: ', 1372.4999999999998, 1372.4999999999998)
      ('  A', 'canvas.bbox:', (0, 0, 915, 915), 'mplCanvas.bbox:', (0, 0, 926, 926))
      ('  B', 'canvas.bbox:', (0, 0, 1372, 1372), 'mplCanvas.bbox:', (0, 0, 926, 926))
      ('  C', 'canvas.bbox:', (0, 0, 1372, 1372), 'mplCanvas.bbox:', (0, 0, 1372, 1372))
      ()
      ## here click "smaller":
      ('old size is', array([ 9.14666667,  9.14666667]))
      ('new size is', array([ 4.57333333,  4.57333333]))
      ('new size pixels: ', 686.0, 686.0)
      ('  A', 'canvas.bbox:', (0, 0, 1372, 1372), 'mplCanvas.bbox:', (0, 0, 1372, 1372))
      ('  B', 'canvas.bbox:', (0, 0, 686, 686), 'mplCanvas.bbox:', (0, 0, 1372, 1372))
      ('  C', 'canvas.bbox:', (0, 0, 686, 686), 'mplCanvas.bbox:', (0, 0, 1372, 1372))
      ()
      

      在 Python3.2 中也可以看到 mplCanvas 的相同行为...不确定这是某种错误,还是我也没有正确理解 :)

      另请注意,这种缩放方式不能处理轴/抽动等字体的大小调整(字体将尝试保持相同的大小);这就是我最终可以用上面的代码得到的(截断的抽搐):

      ...如果添加轴标签等,情况会变得更糟。

      无论如何,希望这会有所帮助,
      干杯!

      【讨论】:

      • 太好了,谢谢!不再认为有人会真正找到解决方案 - 我不得不挖掘出我废弃的旧项目来尝试这个。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2010-11-19
      • 2015-06-08
      • 2023-03-21
      • 2021-01-10
      • 1970-01-01
      • 2021-10-06
      • 1970-01-01
      相关资源
      最近更新 更多