Search code examples
pythontkinterwidgetframeresizable

Tkinter frame, how to make it resize after destroying child widgets and repopulating with new widgets


My app displays data from a sqlite database. After displaying the initial data, say 50 records when my program destroys the widgets associated with that initial display and repopulates based on a search function the frame container no longer adapts to the size of the new widgets placed in it.

To make this simpler I've created smaller simpler version of the problem. My initial version of this post was from that actual app and difficult understand. Lesson learned. Running this code defaults to showin 50 rows initially, Then try entering 100 in the search field and click the button and see that the window does not expand to fit. Then can then try entering 40 and see that the window doesn't shrink. I'm using Python v3.11.

Here's the simplified code that captures the essence of my problem:

from tkinter import *
from tkinter import ttk

def displayData():
    for widgets in second_frame.winfo_children():
      widgets.destroy()
    Count = Search.get()
    print("displayData entered with value of " + str(Count))
    nRows = int(Count)
    rows = []
    for i in range(nRows):
        label_list = Label(second_frame, text='Row '+str(i), relief=GROOVE, font=("Arial 11"), width=17, anchor='w', justify=LEFT)

        label_list.grid(row=i, column=0, sticky=NSEW)
        rows.append(label_list)


def _on_mousewheel(event):
    my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")

# Set up tkinter GUI
root = Tk()
root.geometry("1250x810+100+0")
root.title("Test")

# Control Frame
control_frame = Frame(root, height=10, highlightbackground="blue", highlightthickness=2, pady=3, padx=3)
control_frame.pack(fill=X)

Search = StringVar()
Search.set('50')
ent_search = ttk.Entry(control_frame, width=15, textvariable=Search)


ent_search.pack(padx=5, pady=5, side=RIGHT)

# Create a Main Frame
main_frame = Frame(root, highlightbackground="yellow", highlightthickness=2)
main_frame.pack(fill=BOTH, expand=1)

# Create a Canvas
my_canvas = Canvas(main_frame)
my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
my_canvas.bind_all("<MouseWheel>", _on_mousewheel)

# Add a Scrollbar to the Canvas
my_scrollbar = ttk.Scrollbar(main_frame, orient=VERTICAL, command=my_canvas.yview)
my_scrollbar.pack(side=RIGHT, fill=Y)

# Configure the Canvas
my_canvas.configure(yscrollcommand=my_scrollbar.set)
my_canvas.bind('<Configure>', lambda e: my_canvas.configure(scrollregion = my_canvas.bbox('all')))

# Create Another Frame inside the Canvas
second_frame = Frame(my_canvas)

# Add the New Frame to a Window inside the Canvas
my_canvas.create_window((0,100), window=second_frame, anchor='nw')

btn_search = Button(control_frame, text='Search', command=displayData)
btn_search.pack(padx=5, pady=5, side=RIGHT)

rows = []

displayData()

root.mainloop()

```

FYI: my actual app is displaying database records based on search parameters. The programs works fine except for the fact once the initial size of 'second_frame' is set up it never changes. So if a search happens to display more records than that initial display, those records will be hidden. e.g. Initial display shows 50 records, if a search asks to display 75 records, 25 of them will not be visible...So the second_frame doesn't resize to show the added widgets in the search.

My workaround for now is just to initially display more records then I anticipate most searches will need to display.

How can I make 'second_frame' adapt to new amounts of widgets on new searches? The simplified code above emulates my issue.


Solution

  • Yahoo, found a solution to this issue. Essentially one must first destroy and recreate the canvas used to display the records. Some complications due to the scrollbar widget being linked to the frame containing the canvas. I solved this by creating an intermediate frame to contain the frame referenced by the scrollbar and the used the methods winfo_childred() and destroy() to delete the frames/canvas/widgets before recreating them anew to display the new data.

    Here's the modified example code that now works correctly:

    from tkinter import *
    from tkinter import ttk
    
    def makeCanvas():
        # Create a Canvas
        global my_canvas
        global second_frame
        global my_scrollbar
        global main_frame
    
        mid_frame = Frame(main_frame)
        mid_frame.pack(fill=BOTH, expand=1)
        my_canvas = Canvas(mid_frame)
        my_canvas.pack(side=LEFT, fill=BOTH, expand=1)
        my_canvas.bind_all("<MouseWheel>", _on_mousewheel)
    
        # Add a Scrollbar to the Canvas
        my_scrollbar = ttk.Scrollbar(mid_frame, orient=VERTICAL, command=my_canvas.yview)
        my_scrollbar.pack(side=RIGHT, fill=Y)
    
        # Configure the Canvas
        my_canvas.configure(yscrollcommand=my_scrollbar.set)
        my_canvas.bind('<Configure>', lambda e: my_canvas.configure(scrollregion = my_canvas.bbox('all')))
    
        # Create Another Frame inside the Canvas
        second_frame = Frame(my_canvas)
    
        # Add the New Frame to a Window inside the Canvas
        my_canvas.create_window((0,0), window=second_frame, anchor='nw')
    
    
    def displayData():
        global displayCnt
        print("displayData entered with displayCnt of " + str(displayCnt))
        if displayCnt != 0:
            for widgets in main_frame.winfo_children():
              widgets.destroy()
    
        Count = Search.get()
    
        makeCanvas()
    
        print("displayData entered with value of " + str(Count))
        nRows = int(Count)
        rows = []
        for i in range(nRows):
            label_list = Label(second_frame, text='Row '+str(i), relief=GROOVE, font=("Arial 11"), width=17, anchor='w', justify=LEFT)
    
            label_list.grid(row=i, column=0, sticky=NSEW)
            rows.append(label_list)
    
        displayCnt += 1
    
    
    def _on_mousewheel(event):
        my_canvas.yview_scroll(int(-1*(event.delta/120)), "units")
    
    # Set up tkinter GUI
    root = Tk()
    root.geometry("1250x810+100+0")
    root.title("Test")
    
    # Control Frame
    control_frame = Frame(root, height=10, highlightbackground="blue", highlightthickness=2, pady=3, padx=3)
    control_frame.pack(fill=X)
    
    Search = StringVar()
    Search.set('50')
    ent_search = ttk.Entry(control_frame, width=15, textvariable=Search)
    
    
    ent_search.pack(padx=5, pady=5, side=RIGHT)
    
    # Create a Main Frame
    main_frame = Frame(root, highlightbackground="yellow", highlightthickness=2)
    main_frame.pack(fill=BOTH, expand=1)
    
    # Create a Canvas
    #makeCanvas()
    
    btn_search = Button(control_frame, text='Search', command=displayData)
    btn_search.pack(padx=5, pady=5, side=RIGHT)
    
    rows = []
    
    displayCnt = 0
    
    displayData()
    
    root.mainloop()
    

    I ended up creating a separate function makeCanvas() to recreate the frame/canvas structure upon each new display of data. For this much simplified example the data consists simply of numbered Label widgets.