You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 21:57:03