如何在Tkinter GUI中让Matplotlib图形可以正确滚动和调整大小
如何在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(...)
),但这并没有帮助。我成功尝试了一种替代方案,即在每次调整大小后重新生成整个设置(图形、画布、滚动条)。 (但是,除了看起来有点粗暴外,它还存在一个问题,即我无法正确处理旧图表的处理,导致程序随着时间累积了相当多的内存。)
那么,是否有人对如何使这些滚动条在调整大小操作后正常工作有任何想法?
我遇到了同样的问题 - 就我所看到的(通过实验),除了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中也具有相同的行为...不确定这是否是一种错误,或者我也不理解某些东西 :)
还要注意的是,这种方式的缩放不会处理轴/刻度线等的字体大小重调整(字体将尝试保持相同大小);这是我最终可以从上面的代码中获得的(缩短的刻度线):
...如果添加轴标签等,情况会变得更糟。
无论如何,希望这有所帮助,
干杯!
在 这个回答 中讨论了滚动条之后,我阅读了以下内容:
- 如何在Matplotlib中设置图标题和轴标签的字体大小?
- 如何更改Matplotlib图表上的字体大小
- Python子图为共同轴标签留出空间
- 在Matplotlib中确切的图大小和标题,轴标签
- Matplotlib subplots_adjust hspace使标题和x轴标签不重叠?
我认为我成功地编写了一种缩放代码,它也可以(在某种程度上)缩放标签和填充,因此(近似)整个绘图符合内部大小(请注意,第二个图像使用来自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
(我尝试看看是否可以复制老版本的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()