在可滚动 canvas 内创建可调整大小的 Tkinter 框架

Create resizable Tkinter frame inside of scrollable canvas

我正在使用 Python 和 Tkinter 开发一个应用程序,这个应用程序需要各种输入表单。每个输入表单都需要一个滚动条,以防表单的父表单太短而无法显示所有内容,因此在 Google 的帮助下,这就是我目前拥有的:

import tkinter as tk


def get_vertically_scrollable_frame(parent_frame: tk.Frame or tk.Tk) -> tk.Frame:
    """
    :param parent_frame: The frame to place the canvas and scrollbar onto.
    :return: A scrollable tk.Frame object nested within the parent_frame object.
    """
    assert isinstance(parent_frame, tk.Frame) or isinstance(parent_frame, tk.Tk)

    # Create the canvas and scrollbar.
    canvas = tk.Canvas(master=parent_frame, bg="blue")
    scrollbar = tk.Scrollbar(master=parent_frame, orient=tk.VERTICAL, command=canvas.yview)

    # Let the canvas and scrollbar resize to fit the parent_frame object.
    parent_frame.rowconfigure(0, weight=1)
    parent_frame.columnconfigure(0, weight=1)
    canvas.grid(row=0, column=0, sticky='news')
    scrollbar.grid(row=0, column=1, sticky='nes')

    # Link the canvas and scrollbar together.
    canvas.configure(yscrollcommand=scrollbar.set)
    canvas.bind('<Configure>', lambda x: canvas.configure(scrollregion=canvas.bbox("all")))

    # Create the tk.Frame that is within the canvas.
    canvas_frame = tk.Frame(master=canvas, bg="red")
    canvas.create_window((0, 0), window=canvas_frame, anchor="nw")

    # TODO: Let the canvas_frame object resize to fit the parent canvas.
    canvas.columnconfigure(0, weight=1)
    canvas.rowconfigure(0, weight=1)
    # canvas_frame.grid(row=0, column=0, sticky="news")  # Resizes the frame, but breaks the scrollbar.

    return canvas_frame


if __name__ == "__main__":
    window = tk.Tk()
    window.rowconfigure(0, weight=1)
    window.columnconfigure(0, weight=1)

    parent_frame = tk.Frame(master=window)
    parent_frame.grid(row=0, column=0, sticky="news")

    scrollable_frame = get_vertically_scrollable_frame(parent_frame)

    # Add the widgets to the new frame.
    scrollable_frame.columnconfigure(0, weight=1)  # Resize everything horizontally.
    tk.Label(master=scrollable_frame, text="First name").grid(row=0, column=0, sticky="w")
    tk.Entry(master=scrollable_frame).grid(row=1, column=0, sticky="ew")
    tk.Label(master=scrollable_frame, text="").grid(row=2, column=0, sticky="w")
    tk.Label(master=scrollable_frame, text="Last name").grid(row=3, column=0, sticky="w")
    tk.Entry(master=scrollable_frame).grid(row=4, column=0, sticky="ew")
    tk.Label(master=scrollable_frame, text="").grid(row=5, column=0, sticky="w")
    tk.Label(master=scrollable_frame, text="Email").grid(row=6, column=0, sticky="w")
    tk.Entry(master=scrollable_frame).grid(row=7, column=0, sticky="ew")
    tk.Label(master=scrollable_frame, text="").grid(row=8, column=0, sticky="w")
    tk.Label(master=scrollable_frame, text="Favorite color").grid(row=9, column=0, sticky="w")
    tk.Entry(master=scrollable_frame).grid(row=10, column=0, sticky="ew")
    tk.Label(master=scrollable_frame, text="").grid(row=11, column=0, sticky="w")
    tk.Frame(master=scrollable_frame).grid(row=12, column=0, sticky="news")
    scrollable_frame.rowconfigure(12, weight=1)  # Vertically resize filler frame from last line.
    tk.Button(master=scrollable_frame, text="Clear").grid(row=13, column=0, sticky="ews")
    tk.Button(master=scrollable_frame, text="Submit").grid(row=14, column=0, sticky="ews")

    window.mainloop()

此函数采用一个空的 Tkinter 框架,在该框架上放置一个工作 canvas 和滚动条,将一个新框架放入 canvas,然后 return 将框架放入其中canvas.

虽然滚动条在上面的代码中运行良好,但 return 嵌套的 Tkinter 框架不会调整大小以适应其父级 canvas 的高度和宽度。如果父 canvas 太大,它看起来像这样:

(蓝色区域为canvas,红色为canvas内框。)

为了解决这个问题,我使用网格手动将嵌套框架放置在 canvas 上(请参阅 return 语句之前的注释代码)。 canvas 中的框架开始自行调整大小,但滚动条停止工作。

有没有一种简单的方法可以让 canvas 中的框架在不破坏滚动条的情况下自行调整大小?

Is there a simple way to allow the frame inside the canvas to resize itself without breaking the scrollbar?

简单?我想这取决于您对简单性的定义。这是可能的,但它需要一些额外的代码行。

滚动条仅在您使用 create_window 将框架添加到 canvas 并且仅当您让框架足够大以容纳其所有子项时才起作用,然后相应地设置 canvas bbox。当 window 调整大小时,如果框架小于 canvas,则需要强制框架大于它想要的大小,但如果框架更大,则需要让框架成为其首选大小比 canvas.

解决方案看起来类似于以下示例,超出了我的想象。请注意标签的使用,以便轻松找到内部框架。您可以轻松地存储 create_window 返回的 id 并使用它。这也利用了 event 对象具有 canvas.

widthheight 属性这一事实
def get_vertically_scrollable_frame(parent_frame: tk.Frame or tk.Tk) -> tk.Frame:
    ...
    canvas.create_window((0, 0), window=canvas_frame, anchor="nw", tags=("canvas_frame",))
    canvas.bind('<Configure>', handle_resize)
    ...

def handle_resize(event):
    canvas = event.widget
    canvas_frame = canvas.nametowidget(canvas.itemcget("canvas_frame", "window"))
    min_width = canvas_frame.winfo_reqwidth()
    min_height = canvas_frame.winfo_reqheight()
    if min_width < event.width:
        canvas.itemconfigure("canvas_frame", width=event.width)
    if min_height < event.height:
        canvas.itemconfigure("canvas_frame", height=event.height)

    canvas.configure(scrollregion=canvas.bbox("all"))