Tkinter 中 Matplotlib 图的滚动条 [英] Scrollbars for Matplotlib Figure in Tkinter
问题描述
我正在尝试使滚动条在 Matplotlib
图形的 Tkinter
中工作.滚动条无法适应加载图像的宽度和高度,我也不知道如何解决此问题.任何帮助将不胜感激.
I am trying to get scrollbars to work in Tkinter
for matplotlib
figures. The scrollbars don't adjust to the width and height of the loaded image and I don't know how to troubleshoot this. Any help would be appreciated.
图片:
最终结果:
代码:
import os
from PIL import Image
import tkinter as tk
from tkinter import ttk, filedialog
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import numpy as np
from functools import partial
class AutoScrollbar(ttk.Scrollbar):
def __init__(self,
parent,
*args,
**kwargs):
'''
'''
self.parent = parent
super().__init__(self.parent,
*args,
**kwargs)
def set(self,
low,
high):
'''
'''
if float(low) <= 0.0 and float(high) >= 1.0:
self.tk.call('grid',
'remove',
self)
else:
self.grid()
ttk.Scrollbar.set(self,
low,
high)
class DoubleScrollbarFrame(ttk.Frame):
def __init__(self,
parent,
*args,
**kwargs):
'''
'''
self.parent = parent
super().__init__(self.parent,
*args,
**kwargs)
#Set widgets to fill main window such that they are
#all the same size
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.canvas = tk.Canvas(self)
self.frame = ttk.Frame(self.canvas)
self.scroll_x = AutoScrollbar(self,
orient = tk.HORIZONTAL)
self.scroll_y = AutoScrollbar(self,
orient = tk.VERTICAL)
self.sizegrip = ttk.Sizegrip(self)
self.canvas.config(xscrollcommand = self.scroll_x.set,
yscrollcommand = self.scroll_y.set)
self.scroll_x.config(command = self.canvas.xview)
self.scroll_y.config(command = self.canvas.yview)
self.canvas.create_window((0,0),
window = self.frame,
anchor = 'nw')
self.frame.bind('<Configure>',
self.set_scrollregion)
def position_widgets(self,
**kwargs):
'''
'''
self.scroll_x.grid(row = 1,
column = 0,
sticky = 'ew')
self.scroll_y.grid(row = 0,
column = 1,
sticky = 'ns')
self.canvas.grid(row = 0,
column = 0,
sticky = 'nsew')
self.sizegrip.grid(row = 1,
column = 1,
sticky = 'se')
#NOTE: Do not use geometry manager with `self.frame`. This will
# pass control from the canvas to grid and the canvas will then
# no longer know how much to grow.
def set_scrollregion(self,
event):
'''
'''
self.canvas.configure(scrollregion = self.canvas.bbox('all'))
class Graph(ttk.Frame):
def __init__(self,
parent,
axis_off = True,
*args,
**kwargs):
'''
'''
self.parent = parent
self.axis_off = axis_off
super().__init__(self.parent,
*args,
**kwargs)
#Set widgets to fill main window such that they are
#all the same size
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.graph_container = DoubleScrollbarFrame(self)
self.figure = plt.figure()
self.figure.subplots_adjust(top = 1,
right = 1,
left= 0,
bottom = 0,
wspace = 0,
hspace = 0)
self.axis = self.figure.add_subplot(1,1,1)
if self.axis_off:
self.axis.set_axis_off()
self.mpl_canvas = FigureCanvasTkAgg(self.figure,
self.graph_container.frame)
self.toolbar_frame = ttk.Frame(self)
self.toolbar = NavigationToolbar2Tk(self.mpl_canvas,
self.toolbar_frame)
def position_widgets(self):
'''
'''
self.toolbar.update()
self.toolbar_frame.grid(row = 1,
column = 0,
sticky = 'sew')
self.mpl_canvas.draw()
self.mpl_canvas.get_tk_widget().grid(row = 0,
column = 0,
sticky = 'nsew')
self.graph_container.grid(row = 0,
column = 0,
sticky = 'nsew')
def load_image(self,
path):
'''
'''
self.mpl_canvas.flush_events()
self.axis.clear()
self.image = Image.open(path)
#NOTE: Dots per inch (dpi) in matplotlib should be used for
# printing to paper media only. dpi is set at the time of
# printing/scanning. Computer screen resolution is fixed by
# the screen hardware and given in pixels per inch (ppi).
# An image scanned at higher dpi will appear crisper on a
# computer screen because of the difference in halftone/
# dithering at each pixel, but the total size and number of
# pixels in the image will be the same.
# Matplotlib use 72 pixels per inch (ppi) for its figures by
# default, and this cannot be changed. Thus, if you increase
# dots per inch (dpi), the figure will appear bigger when
# printed to screen (i.e. on the computer monitor) because
# it will use more pixels to represent the same features.
# When printed to paper media, however, the image will be
# the same size regardless of dpi, but have finer halftoning/
# dithering for an improved appearance.
# Matplotlib sets sizes in terms of inches, so the scan dpi
# must be known in order to get the physical size of the
# image.
if self.image.info.get('dpi'):
self.scan_dpi, _ = self.image.info['dpi']
else:
self.scan_dpi = 100
self.w_pel, self.h_pel = self.image.size
self.graph_w_in = self.w_pel / self.scan_dpi
self.graph_h_in = self.h_pel / self.scan_dpi
self.graph_aspect_ratio = self.graph_h_in / self.graph_w_in
self.zoom = 1
self.figure.set_size_inches(self.graph_w_in * self.zoom,
self.graph_h_in * self.zoom)
self.image = np.array(self.image)
self.axis.imshow(self.image)
self.mpl_canvas.draw()
class FileBrowser(tk.Frame):
def __init__(self,
parent,
path_type = 'file',
label_text = '',
file_types = (('*','All File Types...'),),
*args,
**kwargs):
'''
'''
self.parent = parent
self.path_type = path_type
self.label_text = label_text
self.file_types = file_types
super().__init__(parent,
*args,
**kwargs)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.label = tk.Label(self.parent,
text = self.label_text)
self.path_entry = tk.Entry(self.parent,
width = 50)
self.button = ttk.Button(self.parent,
text = 'Browse...',
command = partial(self.open_file_dialog,
self.path_type,
self.file_types))
def position_widgets(self):
'''
'''
opts = {'padx': (5,5),
'pady': (5,5)}
self.label.grid(row = 0,
column = 0,
sticky = 'e',
**opts)
self.path_entry.grid(row = 0,
column = 1,
sticky = 'w',
**opts)
self.button.grid(row = 0,
column = 2,
sticky = 'w',
**opts)
def open_file_dialog(self,
path_type,
file_types):
'''
'''
init_dir = os.getcwd()
if path_type == 'file':
self.path = filedialog.askopenfilename(initialdir = init_dir,
title = 'Select file...',
filetypes = file_types)
elif path_type == 'directory':
self.path = filedialog.askdirectory(initialdir = init_dir,
title = 'Select directory...')
self.path_entry.delete(0,tk.END)
self.path_entry.insert(0,self.path)
def get_path(self):
'''
'''
return self.path_entry.get()
class Loader(ttk.Frame):
'''
'''
def __init__(self,
parent,
*args,
**kwargs):
'''
'''
self.parent = parent
super().__init__(self.parent,
*args,
**kwargs)
#Set widgets to fill main window such that they are
#all the same size
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.input_label_frame = ttk.LabelFrame(self,
text = 'Input')
self.output_label_frame = ttk.LabelFrame(self,
text = 'Output')
self.create_file_browser(self.input_label_frame)
self.create_ok_button(self.input_label_frame)
self.create_output_window(self.output_label_frame)
def create_file_browser(self,
label_frame):
'''
'''
self.browser_frame = ttk.LabelFrame(label_frame,
text = 'Select File')
self.file_browser = FileBrowser(self.browser_frame,
path_type = 'file',
label_text = 'File:',
file_types = (('*.tif', 'TIF'),
('*.png', 'PNG'),))
def create_ok_button(self,
label_frame):
'''
'''
self.input_ok_button = ttk.Button(label_frame,
text = 'OK',
command = self.calibrate)
def calibrate(self):
'''
'''
path = self.file_browser.get_path()
if path == '':
messagebox.showerror(title = 'No File Selected',
message = 'No file chosen. Please select file.')
return
_, ext = os.path.splitext(path)
if ext.lower() not in ('.tif', '.png'):
messagebox.showerror(title = 'File Is Not \"*.tif\" or \"*.png\"',
message = 'File must be a \"*.tif\" or \"*.png\" image file. Please reselect file and try again.')
img = plt.imread(path)
self.graph.load_image(path)
def create_output_window(self,
label_frame):
'''
'''
self.output_frame = ttk.Frame(label_frame)
self.graph = Graph(self.output_frame)
def position_widgets(self,
**kwargs):
'''
'''
#OK Button
self.input_ok_button.grid(row = 4,
column = 0,
sticky = 'e')
#Frames
self.input_label_frame.grid(row = 0,
column = 0,
sticky = 'nsew')
self.browser_frame.grid(row = 1,
column = 0,
sticky = 'nw')
self.file_browser.grid(row = 0,
column = 0,
sticky = 'nsew')
self.output_label_frame.grid(row = 0,
column = 1,
sticky = 'nw')
self.graph.grid(row = 0,
column = 0,
sticky = 'nsew')
self.output_frame.grid(row = 0,
column = 0,
sticky = 'nsew')
class MainApp(tk.Tk):
def __init__(self,
title,
*args,
**kwargs):
'''
'''
self._title = title
super().__init__(*args,
**kwargs)
#Set widgets to fill main window such that they are
#all the same size
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
#Set window title
self.title(self._title)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.loader = Loader(self)
def position_widgets(self):
'''
'''
self.loader.grid(row = 0,
column = 0,
sticky = 'nsew')
if __name__ == '__main__':
#Create GUI
root = MainApp('MainApp')
#Run program
root.mainloop()
推荐答案
matplotlib
图形是画布.因此,它的容器框架将无法调整大小,并且 bbox("all")
将不起作用.相反,我们必须更新 matplotlib
canvas
大小以匹配 figure
大小和顶层 canvas
滚动区域
到 figure
大小.
matplotlib
figures are canvases. Therfore, its container frame won't resize to it, and bbox("all")
won't work. Instead, we have to update the matplotlib
canvas
size to match the figure
size and the toplevel canvas
scrollregion
to the figure
size.
这是工作输出.
工作输出
这是工作代码.
代码
import os
from PIL import Image
import tkinter as tk
from tkinter import ttk, filedialog
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import numpy as np
from functools import partial
#Matplotlib Default DPI
DEFAULT_DPI = 100
#Matplotlib Default PPI
DEFAULT_PPI = 72
class AutoScrollbar(ttk.Scrollbar):
def __init__(self,
parent,
*args,
**kwargs):
'''
'''
self.parent = parent
super().__init__(self.parent,
*args,
**kwargs)
def set(self,
low,
high):
'''
'''
if float(low) <= 0.0 and float(high) >= 1.0:
self.tk.call('grid',
'remove',
self)
else:
self.grid()
ttk.Scrollbar.set(self,
low,
high)
class DoubleScrollbarFrame(ttk.Frame):
def __init__(self,
parent,
*args,
**kwargs):
'''
'''
self.parent = parent
super().__init__(self.parent,
*args,
**kwargs)
#Set widgets to fill main window such that they are
#all the same size
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.canvas = tk.Canvas(self)
self.frame = ttk.Frame(self.canvas)
self.scroll_x = AutoScrollbar(self,
orient = tk.HORIZONTAL)
self.scroll_y = AutoScrollbar(self,
orient = tk.VERTICAL)
self.sizegrip = ttk.Sizegrip(self)
self.canvas.config(xscrollcommand = self.scroll_x.set,
yscrollcommand = self.scroll_y.set)
self.scroll_x.config(command = self.canvas.xview)
self.scroll_y.config(command = self.canvas.yview)
self.canvas.create_window((0,0),
window = self.frame,
anchor = 'nw')
self.frame.bind('<Configure>',
self.set_scrollregion)
def position_widgets(self,
**kwargs):
'''
'''
self.scroll_x.grid(row = 1,
column = 0,
sticky = 'ew')
self.scroll_y.grid(row = 0,
column = 1,
sticky = 'ns')
self.canvas.grid(row = 0,
column = 0,
sticky = 'nsew')
self.sizegrip.grid(row = 1,
column = 1,
sticky = 'se')
#NOTE: Do not use geometry manager with `self.frame`. This will
# pass control from the canvas to grid and the canvas will then
# no longer know how much to grow.
def set_scrollregion(self,
event):
'''
'''
self.canvas.configure(scrollregion = self.canvas.bbox('all'))
class Graph(ttk.Frame):
def __init__(self,
parent,
axis_off = True,
*args,
**kwargs):
'''
'''
self.parent = parent
self.axis_off = axis_off
super().__init__(self.parent,
*args,
**kwargs)
#Set widgets to fill main window such that they are
#all the same size
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.figure = plt.figure()
self.figure.subplots_adjust(top = 1,
right = 1,
left= 0,
bottom = 0,
wspace = 0,
hspace = 0)
self.axis = self.figure.add_subplot(1,1,1)
if self.axis_off:
self.axis.set_axis_off()
self.canvas = tk.Canvas(self)
self.frame = ttk.Frame(self.canvas)
self.mpl_canvas = FigureCanvasTkAgg(self.figure,
self.frame)
self.mpl_canvas.draw()
self.scroll_x = AutoScrollbar(self,
orient = tk.HORIZONTAL)
self.scroll_y = AutoScrollbar(self,
orient = tk.VERTICAL)
self.sizegrip = ttk.Sizegrip(self)
self.canvas.config(xscrollcommand = self.scroll_x.set,
yscrollcommand = self.scroll_y.set)
self.scroll_x.config(command = self.canvas.xview)
self.scroll_y.config(command = self.canvas.yview)
self.cwid = self.canvas.create_window((0,0),
window = self.frame,
anchor = 'nw')
self.frame.bind('<Configure>',
self.set_scrollregion)
self.toolbar_frame = ttk.Frame(self)
self.toolbar = NavigationToolbar2Tk(self.mpl_canvas,
self.toolbar_frame)
def set_scrollregion(self,
event):
'''
'''
w, h = self.figure.get_size_inches()
w = int(w * DEFAULT_DPI)
h = int(h * DEFAULT_DPI)
scrollregion = (0,0,w,h)
self.canvas.configure(scrollregion = scrollregion)
def position_widgets(self):
'''
'''
self.scroll_x.grid(row = 1,
column = 0,
sticky = 'ew')
self.scroll_y.grid(row = 0,
column = 1,
sticky = 'ns')
self.canvas.grid(row = 0,
column = 0,
sticky = 'nsew')
self.mpl_canvas.get_tk_widget().grid(row = 0,
column = 0,
sticky = 'nsew')
self.sizegrip.grid(row = 1,
column = 1,
sticky = 'se')
#NOTE: Do not use geometry manager with `self.frame`. This will
# pass control from the canvas to grid and the canvas will then
# no longer know how much to grow.
self.toolbar.update()
self.toolbar_frame.grid(row = 2,
column = 0,
sticky = 'sew')
def load_image(self,
path):
'''
'''
self.mpl_canvas.flush_events()
self.axis.clear()
self.image = Image.open(path)
#NOTE: Dots per inch (dpi) in matplotlib should be used for
# printing to paper media only. dpi is set at the time of
# printing/scanning. Computer screen resolution is fixed by
# the screen hardware and given in pixels per inch (ppi).
# An image scanned at higher dpi will appear crisper on a
# computer screen because of the difference in halftone/
# dithering at each pixel, but the total size and number of
# pixels in the image will be the same.
# Matplotlib use 72 pixels per inch (ppi) for its figures by
# default, and this cannot be changed. Thus, if you increase
# dots per inch (dpi), the figure will appear bigger when
# printed to screen (i.e. on the computer monitor) because
# it will use more pixels to represent the same features.
# When printed to paper media, however, the image will be
# the same size regardless of dpi, but have finer halftoning/
# dithering for an improved appearance.
# Matplotlib sets sizes in terms of inches, so the scan dpi
# must be known in order to get the physical size of the
# image.
if self.image.info.get('dpi'):
self.scan_dpi, _ = self.image.info['dpi']
else:
self.scan_dpi = DEFAULT_DPI # matplotlib default
self.w_pel, self.h_pel = self.image.size
self.graph_w_in = self.w_pel / self.scan_dpi
self.graph_h_in = self.h_pel / self.scan_dpi
self.figure.set_size_inches(self.graph_w_in,
self.graph_h_in)
self.image = np.array(self.image)
self.axis.imshow(self.image)
self.mpl_canvas.get_tk_widget().configure(
width = self.graph_w_in * DEFAULT_DPI,
height = self.graph_h_in * DEFAULT_DPI)
self.mpl_canvas.draw()
self.canvas.update()
class FileBrowser(tk.Frame):
def __init__(self,
parent,
path_type = 'file',
label_text = '',
file_types = (('*','All File Types...'),),
*args,
**kwargs):
'''
'''
self.parent = parent
self.path_type = path_type
self.label_text = label_text
self.file_types = file_types
super().__init__(parent,
*args,
**kwargs)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.label = tk.Label(self.parent,
text = self.label_text)
self.path_entry = tk.Entry(self.parent,
width = 50)
self.button = ttk.Button(self.parent,
text = 'Browse...',
command = partial(self.open_file_dialog,
self.path_type,
self.file_types))
def position_widgets(self):
'''
'''
opts = {'padx': (5,5),
'pady': (5,5)}
self.label.grid(row = 0,
column = 0,
sticky = 'e',
**opts)
self.path_entry.grid(row = 0,
column = 1,
sticky = 'w',
**opts)
self.button.grid(row = 0,
column = 2,
sticky = 'w',
**opts)
def open_file_dialog(self,
path_type,
file_types):
'''
'''
init_dir = os.getcwd()
if path_type == 'file':
self.path = filedialog.askopenfilename(initialdir = init_dir,
title = 'Select file...',
filetypes = file_types)
elif path_type == 'directory':
self.path = filedialog.askdirectory(initialdir = init_dir,
title = 'Select directory...')
self.path_entry.delete(0,tk.END)
self.path_entry.insert(0,self.path)
def get_path(self):
'''
'''
return self.path_entry.get()
class Loader(ttk.Frame):
'''
'''
def __init__(self,
parent,
*args,
**kwargs):
'''
'''
self.parent = parent
super().__init__(self.parent,
*args,
**kwargs)
#Set widgets to fill main window such that they are
#all the same size
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.input_label_frame = ttk.LabelFrame(self,
text = 'Input')
self.output_label_frame = ttk.LabelFrame(self,
text = 'Output')
self.create_file_browser(self.input_label_frame)
self.create_ok_button(self.input_label_frame)
self.create_output_window(self.output_label_frame)
def create_file_browser(self,
label_frame):
'''
'''
self.browser_frame = ttk.LabelFrame(label_frame,
text = 'Select File')
self.file_browser = FileBrowser(self.browser_frame,
path_type = 'file',
label_text = 'File:',
file_types = (('*.tif', 'TIF'),
('*.png', 'PNG'),))
def create_ok_button(self,
label_frame):
'''
'''
self.input_ok_button = ttk.Button(label_frame,
text = 'OK',
command = self.calibrate)
def calibrate(self):
'''
'''
path = self.file_browser.get_path()
if path == '':
messagebox.showerror(title = 'No File Selected',
message = 'No file chosen. Please select file.')
return
_, ext = os.path.splitext(path)
if ext.lower() not in ('.tif', '.png'):
messagebox.showerror(title = 'File Is Not \"*.tif\" or \"*.png\"',
message = 'File must be a \"*.tif\" or \"*.png\" image file. Please reselect file and try again.')
img = plt.imread(path)
self.graph.load_image(path)
def create_output_window(self,
label_frame):
'''
'''
self.output_frame = ttk.Frame(label_frame)
self.graph = Graph(self.output_frame)
def position_widgets(self,
**kwargs):
'''
'''
#OK Button
self.input_ok_button.grid(row = 4,
column = 0,
sticky = 'e')
#Frames
self.input_label_frame.grid(row = 0,
column = 0,
sticky = 'nsew')
self.browser_frame.grid(row = 1,
column = 0,
sticky = 'nw')
self.file_browser.grid(row = 0,
column = 0,
sticky = 'nsew')
self.output_label_frame.grid(row = 0,
column = 1,
sticky = 'nw')
self.graph.grid(row = 0,
column = 0,
sticky = 'nsew')
self.output_frame.grid(row = 0,
column = 0,
sticky = 'nsew')
class MainApp(tk.Tk):
def __init__(self,
title,
*args,
**kwargs):
'''
'''
self._title = title
super().__init__(*args,
**kwargs)
#Set widgets to fill main window such that they are
#all the same size
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
#Set window title
self.title(self._title)
self.create_widgets()
self.position_widgets()
def create_widgets(self):
'''
'''
self.loader = Loader(self)
def position_widgets(self):
'''
'''
self.loader.grid(row = 0,
column = 0,
sticky = 'nsew')
if __name__ == '__main__':
#Create GUI
root = MainApp('MainApp')
#Run program
root.mainloop()
这篇关于Tkinter 中 Matplotlib 图的滚动条的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!