如何在Tkinter GUI中让Matplotlib图形可以正确滚动和调整大小

11 浏览
0 Comments

如何在Tkinter GUI中让Matplotlib图形可以正确滚动和调整大小

我有一个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(...)),但这并没有帮助。我成功尝试了一种替代方案,即在每次调整大小后重新生成整个设置(图形、画布、滚动条)。 (但是,除了看起来有点粗暴外,它还存在一个问题,即我无法正确处理旧图表的处理,导致程序随着时间累积了相当多的内存。)

那么,是否有人对如何使这些滚动条在调整大小操作后正常工作有任何想法?

admin 更改状态以发布 2023年5月21日
0
0 Comments

我遇到了同样的问题 - 就我所看到的(通过实验),除了figure.set_size_inches()之外,你还必须设置mplCanvas和创建它的窗口的新尺寸,在进行figure.canvas.draw()之前(然后也迫使人们使用全局变量 - 或类定义)。另外,显然不需要给mplCanvas“grid” - 因为它已经是canvas的子对象,而canvas已经被“grid”了。而且可能会想要将其锚定在NW上,这样在每次调整大小时,绘图就会在左上角的位置重新绘制。

这是对我有用的(我也尝试了与Python Tkinter scrollbar for frame中的“内部”帧一样,但这不起作用;在片段的末尾留下了其中的一些)代码:

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

mplCanvas在Python3.2中也具有相同的行为...不确定这是否是一种错误,或者我也不理解某些东西 :)

还要注意的是,这种方式的缩放不会处理轴/刻度线等的字体大小重调整(字体将尝试保持相同大小);这是我最终可以从上面的代码中获得的(缩短的刻度线):

screenshot of code

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

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

0
0 Comments

这个回答 中讨论了滚动条之后,我阅读了以下内容:

我认为我成功地编写了一种缩放代码,它也可以(在某种程度上)缩放标签和填充,因此(近似)整个绘图符合内部大小(请注意,第二个图像使用来自imgur的“中等”缩放):

smallest medium
large

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

请注意,对于较新的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

(我尝试看看是否可以复制老版本的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()

0