Tkinter框架内Canvas显示过小 需实现带横向滚动布局
Tkinter中嵌套Matplotlib画布并实现横向滚动的解决方案
我尝试在Tkinter框架内嵌套Canvas与子框架,用于放置Matplotlib绘图Canvas并启用横向滚动,但当前Canvas显示区域过小。此前参考相关问题仍未解决,希望将界面分为左右/上下两个主框架,在顶部/右侧框架内实现带横向滚动的Canvas。
原代码
import tkinter as tk from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk) from matplotlib.figure import Figure import matplotlib.pyplot as plt import numpy as np # --- functions --- def calculate_gap(sum_of_demand, allocated, capacity): if sum_of_demand - allocated < 0: sum_of_not_demanded = abs(sum_of_demand - allocated) elif capacity - sum_of_demand > 0: sum_of_not_demanded = capacity - sum_of_demand else: sum_of_not_demanded = 0 if capacity - allocated > 0: spare_capacity = capacity - allocated else: spare_capacity = 0 if sum_of_demand - capacity > 0: skill_gap = sum_of_demand - capacity else: skill_gap = 0 return sum_of_not_demanded, spare_capacity, skill_gap def test_gap(): sum_of_demand = 256 allocated = 73 capacity = 66 sum_of_not_demanded, spare_capacity, skill_gap = calculate_gap(sum_of_demand, allocated, capacity) width = 0.35 below = np.array([sum_of_demand, allocated, capacity]) above = np.array([sum_of_not_demanded, spare_capacity, skill_gap]) weight_counts = { "Below": np.array([sum_of_demand, allocated, capacity]), "Above": np.array([sum_of_not_demanded, spare_capacity, skill_gap]) } title = ['Demand', 'Allocated', 'Capacity'] bottom = np.zeros(3) for boolean, weight_count in weight_counts.items(): p = graph4_ax.bar(title, weight_count, width, label=boolean, bottom=bottom) bottom += weight_count graph4_canvas.draw() # canvas1.get_tk_widget().pack(side="left",fill="both",expand=True) graph4_canvas.get_tk_widget().pack(fill="both", expand=True, side="left") # graph4_canvas.get_tk_widget().config(scrollregion=graph4_canvas.get_tk_widget().bbox("all")) # graph4_canvas.get_tk_widget().config(xscrollcommand=horizontal_scrollbar.set, # scrollregion=graph4_canvas.get_tk_widget().bbox("all")) # test_canvas.get_tk_widget().config(xscrollcommand=horizontal_scrollbar.set) # test_canvas.get_tk_widget().bind("<<Key>>", pop_up) def mobile_gap(): sum_of_demand = 256 allocated = 73 capacity = 66 sum_of_not_demanded, spare_capacity, skill_gap = calculate_gap(sum_of_demand, allocated, capacity) if sum_of_demand - capacity > 0: skill_gap = sum_of_demand - capacity else: skill_gap = 0 width = 0.1 weight_counts = { "Below": np.array([sum_of_demand, allocated, capacity]), "Above": np.array([sum_of_not_demanded, spare_capacity, skill_gap]) } title = ['Demand', 'Allocated', 'Capacity'] bottom = np.zeros(3) for boolean, weight_count in weight_counts.items(): p = graph_ax.bar(title, weight_count, width, label=boolean, bottom=bottom) bottom += weight_count graph3_canvas.draw() # canvas1.get_tk_widget().pack(side="left",fill="both",expand=True) graph3_canvas.get_tk_widget().pack(fill="both", expand=True, side="left") # graph3_canvas.get_tk_widget().config(scrollregion=graph3_canvas.get_tk_widget().bbox("all")) # graph3_canvas.get_tk_widget().config(xscrollcommand=horizontal_scrollbar.set, # scrollregion=graph3_canvas.get_tk_widget().bbox("all")) # mobile_canvas.get_tk_widget().config(xscrollcommand=horizontal_scrollbar.set) def graph_two(): sum_of_demand = 830 allocated = 313 capacity = 339 sum_of_not_demanded, spare_capacity, skill_gap = calculate_gap(sum_of_demand, allocated, capacity) width = 0.35 weight_counts = { "Below": np.array([sum_of_demand, allocated, capacity]), "Above": np.array([sum_of_not_demanded, spare_capacity, skill_gap]) } title = ['Demand', 'Allocated', 'Capacity'] bottom = np.zeros(3) for boolean, weight_count in weight_counts.items(): p = graph2_ax.bar(title, width, label=boolean, bottom=bottom) bottom += weight_count graph2_canvas.draw() # canvas1.get_tk_widget().pack(side="left",fill="both",expand=True) graph2_canvas.get_tk_widget().pack(fill="both", expand=True, side="left") # graph2_canvas.get_tk_widget().config(scrollregion=graph2_canvas.get_tk_widget().bbox("all")) # graph2_canvas.get_tk_widget().config(xscrollcommand=horizontal_scrollbar.set, # scrollregion=graph2_canvas.get_tk_widget().bbox("all")) # be_canvas.get_tk_widget().config(xscrollcommand=horizontal_scrollbar.set) def graph_one(): sum_of_demand = 1631 allocated = 778 capacity = 795 sum_of_not_demanded, spare_capacity, skill_gap = calculate_gap(sum_of_demand, allocated, capacity) width = 0.35 weight_counts = { "Below": np.array([sum_of_demand, allocated, capacity]), "Above": np.array([sum_of_not_demanded, spare_capacity, skill_gap]) } title = ['Demand', 'Allocated', 'Capacity'] bottom = np.zeros(3) for boolean, weight_count in weight_counts.items(): p = graph1_ax.bar(title, weight_count, width, label=boolean, bottom=bottom) bottom += weight_count graph1_canvas.draw() graph1_canvas.get_tk_widget().pack(fill="both", expand=True, side="left") # graph1_canvas.get_tk_widget().config(scrollregion=graph1_canvas.get_tk_widget().bbox("all")) # graph1_canvas.get_tk_widget().config(xscrollcommand=horizontal_scrollbar.set, # scrollregion=graph1_canvas.get_tk_widget().bbox("all")) def resize(event): dashboard_canvas.configure(scrollregion=dashboard_canvas.bbox('all')) root = tk.Tk() root.geometry("1000x1000") root.title("eggs") main_frame = tk.Frame(root) main_frame.pack() # --- frame1 = tk.LabelFrame(main_frame,text="Frame One") frame2 = tk.LabelFrame(main_frame,text="Frame two") frame1.pack(side=tk.TOP, expand=True,fill=tk.BOTH) frame2.pack(side=tk.TOP, expand=True,fill=tk.BOTH) # canvas dashboard_canvas = tk.Canvas(frame1)#, bg='#00c000') # background color only to test its size dashboard_inner_frame = tk.Frame(dashboard_canvas) inner_frame_id = dashboard_canvas.create_window((0,0), window=dashboard_inner_frame, anchor='nw') dashboard_canvas.pack(fill='both', expand=True) # scrollbar dasboard_scrollbar_x = tk.Scrollbar(root, orient='horizontal') dasboard_scrollbar_x.pack(fill='x') # join widgets dashboard_canvas.configure(xscrollcommand=dasboard_scrollbar_x.set) dasboard_scrollbar_x['command'] = dashboard_canvas.xview # resize scrollregion on canvas when plots will have size (and it will be after starting program and drawing plots) dashboard_inner_frame.bind('<Configure>', resize) #--- # put on dashboard_inner_frame frame_top = tk.Frame(dashboard_inner_frame, width=2000) frame_top.pack(fill='both', expand=True) # dashboard_inner_frame.pack(side=tk.LEFT) # plots = [] graph1_fig, graph1_ax = plt.subplots(dpi=100) graph1_canvas = FigureCanvasTkAgg(graph1_fig, frame_top) # horizontal_scrollbar.config(command=graph1_canvas.get_tk_widget().xview) # graph1_canvas.get_tk_widget().config(yscrollcommand=horizontal_scrollbar.set) # BE Canvas graph2_fig, graph2_ax = plt.subplots(dpi=100) graph2_canvas = FigureCanvasTkAgg(graph2_fig, frame_top) # Canvas graph3_fig, graph_ax = plt.subplots(dpi=100) graph3_canvas = FigureCanvasTkAgg(graph3_fig, frame_top) # Canvas graph4_fig, graph4_ax = plt.subplots(dpi=100) graph4_canvas = FigureCanvasTkAgg(graph4_fig, frame_top) graph_one() graph_two() mobile_gap() test_gap() # for index in range(10): # fig = Figure(dpi=100) # ax = fig.add_subplot(111) # ax.plot(x, y) # fig.suptitle(f"Plot {index+1}") # # canvas = FigureCanvasTkAgg(fig, master=frame_top) # print(canvas) # canvas.draw() # canvas.get_tk_widget().pack(side="left", fill='both', expand=True) #canvas.get_tk_widget()['width'] = 1 # plots.append({'fig': fig, 'canvas': canvas}) root.mainloop()
问题分析
- 滚动条挂载位置错误:原代码将横向滚动条放在
root下,导致它和目标Canvas不在同一父容器,布局逻辑混乱 - 内部框架布局参数错误:
frame_top使用fill='both'会强制它适应父容器宽度,无法横向扩展容纳所有Matplotlib画布 - Matplotlib画布的压缩问题:虽然设置了
side="left",但缺少固定宽度或防止压缩的配置,导致画布被挤压变小 - ScrollRegion更新不及时:需要确保所有绘图完成后,正确触发滚动区域的更新
修改后的代码
import tkinter as tk from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg import matplotlib.pyplot as plt import numpy as np # --- functions --- def calculate_gap(sum_of_demand, allocated, capacity): if sum_of_demand - allocated < 0: sum_of_not_demanded = abs(sum_of_demand - allocated) elif capacity - sum_of_demand > 0: sum_of_not_demanded = capacity - sum_of_demand else: sum_of_not_demanded = 0 spare_capacity = capacity - allocated if capacity - allocated > 0 else 0 skill_gap = sum_of_demand - capacity if sum_of_demand - capacity > 0 else 0 return sum_of_not_demanded, spare_capacity, skill_gap def plot_bar(ax, canvas, sum_of_demand, allocated, capacity, width=0.35): sum_of_not_demanded, spare_capacity, skill_gap = calculate_gap(sum_of_demand, allocated, capacity) weight_counts = { "Below": np.array([sum_of_demand, allocated, capacity]), "Above": np.array([sum_of_not_demanded, spare_capacity, skill_gap]) } title = ['Demand', 'Allocated', 'Capacity'] bottom = np.zeros(3) for boolean, weight_count in weight_counts.items(): ax.bar(title, weight_count, width, label=boolean, bottom=bottom) bottom += weight_count ax.legend() canvas.draw() # 让画布保持自身宽度,不被压缩 canvas.get_tk_widget().pack(side="left", fill="y", expand=False, padx=5, pady=5) def resize(event): dashboard_canvas.configure(scrollregion=dashboard_canvas.bbox('all')) root = tk.Tk() root.geometry("1000x1000") root.title("eggs") main_frame = tk.Frame(root) main_frame.pack(fill='both', expand=True) # 上下布局两个主框架 frame1 = tk.LabelFrame(main_frame, text="带横向滚动的绘图区") frame2 = tk.LabelFrame(main_frame, text="底部框架") frame1.pack(side=tk.TOP, expand=True, fill=tk.BOTH, padx=5, pady=5) frame2.pack(side=tk.TOP, expand=True, fill=tk.BOTH, padx=5, pady=5) # 创建带横向滚动的Canvas容器 dashboard_canvas = tk.Canvas(frame1) dashboard_inner_frame = tk.Frame(dashboard_canvas) inner_frame_id = dashboard_canvas.create_window((0, 0), window=dashboard_inner_frame, anchor='nw') # 横向滚动条放在frame1内,和Canvas对齐 dasboard_scrollbar_x = tk.Scrollbar(frame1, orient='horizontal', command=dashboard_canvas.xview) dashboard_canvas.configure(xscrollcommand=dasboard_scrollbar_x.set) # 布局Canvas和滚动条 dashboard_canvas.pack(side=tk.TOP, fill='both', expand=True) dasboard_scrollbar_x.pack(side=tk.TOP, fill='x') # 绑定内部框架的配置事件,更新滚动区域 dashboard_inner_frame.bind('<Configure>', resize) # 创建放置Matplotlib画布的横向容器 frame_top = tk.Frame(dashboard_inner_frame) frame_top.pack(fill='y', expand=True) # 创建绘图 graph1_fig, graph1_ax = plt.subplots(figsize=(6,4), dpi=100) graph1_canvas = FigureCanvasTkAgg(graph1_fig, frame_top) plot_bar(graph1_ax, graph1_canvas, 1631, 778, 795) graph2_fig, graph2_ax = plt.subplots(figsize=(6,4), dpi=100) graph2_canvas = FigureCanvasTkAgg(graph2_fig, frame_top) plot_bar(graph2_ax, graph2_canvas, 830, 313, 339) graph3_fig, graph3_ax = plt.subplots(figsize=(6,4), dpi=100) graph3_canvas = FigureCanvasTkAgg(graph3_fig, frame_top) plot_bar(graph3_ax, graph3_canvas, 256, 73, 66, width=0.1) graph4_fig, graph4_ax = plt.subplots(figsize=(6,4), dpi=100) graph4_canvas = FigureCanvasTkAgg(graph4_fig, frame_top) plot_bar(graph4_ax, graph4_canvas, 256, 73, 66) root.mainloop()
关键修改点
- 滚动条容器修正:将横向滚动条移到
frame1内,和目标Canvas保持同一父容器,确保布局关联正确 - 容器布局优化:
frame_top改用fill='y',只在垂直方向填充,允许水平方向扩展以容纳所有画布 - Matplotlib画布配置:每个绘图画布使用
fill="y"和expand=False,保持自身宽度不被压缩,同时添加padx/pady增加间距 - 绘图函数重构:将重复的绘图逻辑封装成
plot_bar函数,减少冗余代码 - Figure尺寸设置:创建Matplotlib图时指定
figsize,确保画布有足够的显示空间 - 滚动区域更新:保留
Configure事件绑定,确保内部框架尺寸变化时自动更新滚动区域
内容的提问来源于stack exchange,提问作者Chandrakanth Reddy
相关产品推荐
相关产品推荐

