简体   繁体   中英

How to get a Matplotlib figure to scroll + resize properly in a Tkinter GUI

I have a Tkinter GUI which displays a Matplotlib plot (Python 2.7.3 with Matplotlib 1.2.0rc2) and lets the user configure certain aspects of the plot. The plots tend to get large, so the figure is wrapped in a scrolling canvas. One aspect of configuring the plot is changing its size.

Now while the plot scrolls properly on the one hand, and the resizing works as well on the other, the two operations don't work in combination. Below is a script to demonstrate the effect. (Sorry about the length, I couldn't get it any shorter.) You can scroll through the plot (using the scrollbars), and it can be made smaller and larger (using the buttons). However, whenever you scroll, the figure is reset to its original size . Evidently, I would like the size of the figure to not change by use of the scrollbars.

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

I think I'm missing something about how the plot & the scrolling canvas are tied together; I have tried reconfiguring the scrolling canvas (with canvas.create_window(...) and canvas.config(...) ) after each changeSize call, but that did not help. One alternative I did get to work was to regenerate the whole setup (figure, canvas, scrollbars) after each resize. (However, apart from seeming a little brutal, that had the problem that I couldn't get old figures to be disposed of properly, making the program accumulate quite a lot of memory over time.)

So, does anybody have any ideas about how get those scrollbars to behave properly after resize operations?

Right; after the scrollbar discussion in this answer , I ended up going through this:

.. and I think I managed to get a sort of a scaling code that also scales (somewhat) labels and padding, so (approximately) the whole plot fits inside (note, second image uses "medium" scale from imgur):

最少介质大

For very small sizes, labels again start disappearing - but it still holds OK for a range of sizes.

Note that for newer matplotlib (>= 1.1.1), there is a function figure.tight_layout() that does the margins (but not the font size) for cases like this (it is a single subplot) - but if you're using an older matplotlib , you can do figure.subplots_adjust(left=0.2, bottom=0.15, top=0.86) which is what this example does; and has been tested in:

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

( I did try to see if I can copy tight_layout for older matplotlib - unfortunately, it requires a rather complex set of functions included from tight_layout.py , which in turn require that Figure and Axes also have specific specifications, not present in v.0.99 )

Since subplots_adjust takes relative parameters (from 0.0 to 1.0), we can in principle just set them once - and hope they hold for our desired scale range. For the rest (scaling of fonts and labelpad) see the code below:

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

I just bumped in the same problem - and as far as I can see (by experimentation), beyond figure.set_size_inches() , you must also set the new size of mplCanvas and of the canvas-created window for it, before doing figure.canvas.draw() (which then also forces one to use global vars - or class definitions). Also, no need to "grid" mplCanvas apparently - as it is already a child of canvas , which is already "grid"ded. And probably want to anchor NW, so at each resize, the plot is redrawn at 0,0 at top-left corner.

Here is what worked for me (I also tried with "internal" frame as in Python Tkinter scrollbar for frame , but that didn't work; some of that is left at end of snippet):

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

An interesting note is that mplCanvas will obey the sizing if it grows larger (as in click on "larger") - but keep the old size if it gets smaller:

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

Same behavior of mplCanvas can be seen in Python3.2, as well... not sure if this is a bug of sorts, or I too am not understanding something right :)

Note also that this scaling in this way doesn't handle resizing of fonts of axes/tics etc (fonts will try to remain the same size); this is what I can eventually get with above code (truncated tics):

代码截图

... and it gets even worse if you add axis labels, etc.

Anyways, hope this helps,
Cheers!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM