Home > Net >  Create resizable Tkinter frame inside of scrollable canvas
Create resizable Tkinter frame inside of scrollable canvas

Time:10-13

There is an application I am working on using Python and Tkinter, and this application requires a variety of input forms. Each input form needs a scrollbar in case the form’s parent is too short to display everything, and so after some help from Google, this is what I currently have:

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

This function takes an empty Tkinter frame, places a working canvas and scrollbar on that frame, places a new frame into the canvas, and then returns the frame inside the canvas.

While the scrollbar works fine with the above code, the returned nested Tkinter frame does not resize to fit the height and width of its parent canvas. If the parent canvas is too large, it looks like this:

Working scrollbar 1

Working scrollbar 2

(The blue area is the canvas, and red is the frame inside the canvas.)

In an attempt to fix this, I manually placed the nested frame on the canvas using grid (see the commented code just before the return statement). The frame inside the canvas started resizing itself, but the scrollbar stopped working.

Broken scrollbar 1

Broken scrollbar 2

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

CodePudding user response:

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

Simple? I guess that depends on your definition of simplicity. It's possible, but it requires a few extra lines of code.

The scrollbars only work if you add the frame to the canvas with create_window and only when you let the frame be as big as it needs to be to hold all of its children and then set the canvas bbox accordingly. When the window resizes you need to force the frame to be bigger than it wants to be if it is smaller than the canvas, but you need to let the frame be its preferred size if the frame is bigger than the canvas.

The solution looks something like the following example, off the top of my head. Notice the use of a tag to make it easy to find the inner frame. You could just as easily store the id returned by create_window and use that instead. This also takes advantage of the fact that the event object has width and height attributes for the canvas.

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"))
  • Related