Search code examples
pythonpython-3.xtkintertkinter-layout

How to shrink a frame in tkinter after removing contents?


Most of the topics I came across deals with how to not shrink the Frame with contents, but I'm interested in shrinking it back after the destruction of said contents. Here's an example:

import tkinter as tk
root = tk.Tk()
lbl1 = tk.Label(root, text='Hello!')
lbl1.pack()
frm = tk.Frame(root, bg='black')
frm.pack()
lbl3 = tk.Label(root, text='Bye!')
lbl3.pack()
lbl2 = tk.Label(frm, text='My name is Foo')
lbl2.pack()

So far I should see this in my window:

Hello!
My name is Foo
Bye!

That's great, but I want to keep the middle layer interchangeable and hidden based on needs. So if I destroy the lbl2 inside:

lbl2.destroy()

I want to see:

Hello!
Bye!

But what I see instead:

Hello!
███████
Bye!

I want to shrink frm back to basically non-existence because I want to keep the order of my main widgets intact. Ideally, I want to run frm.pack(fill=tk.BOTH, expand=True) so that my widgets inside can scale accordingly. However if this interferes with the shrinking, I can live without fill/expand.

I've tried the following:

  1. pack_propagate(0): This actually doesn't expand the frame at all past pack().
  2. Re-run frm.pack(): but this ruins the order of my main widgets.
  3. .geometry(''): This only works on the root window - doesn't exist for Frames.
  4. frm.config(height=0): Oddly, this doesn't seem to change anything at all.
  5. frm.pack_forget(): From this answer, however it doesn't bring it back.

The only option it leaves me is using a grid manager, which works I suppose, but not exactly what I'm looking for... so I'm interested to know if there's another way to achieve this.


Solution

  • Question: Shrink a Frame after removing the last widget?

    Bind to the <'Expose'> event and .configure(height=1) if no children.


    Reference:

    • Expose

      An Expose event is generated whenever all or part of a widget should be redrawn


    import tkinter as tk
    
    class App(tk.Tk):
        def __init__(self):
            super().__init__()
    
            tk.Label(self, text='Hello!').pack()
            self.frm = frm = tk.Frame(self, bg='black')
            frm.pack()
            tk.Label(self, text='Bye!').pack()
            tk.Label(frm, text='My name is Foo').pack()
    
            self.menubar = tk.Menu()
            self.config(menu=self.menubar)
            self.menubar.add_command(label='delete', command=self.do_destroy)
            self.menubar.add_command(label='add', command=self.do_add)
    
            frm.bind('<Expose>', self.on_expose)
    
        def do_add(self):
            tk.Label(self.frm, text='My name is Foo').pack()
            
        def do_destroy(self):
            w = self.frm
            if w.children:
                child = list(w.children).pop(0)
                w.children[child].destroy()
    
        def on_expose(self, event):
            w = event.widget
            if not w.children:
                w.configure(height=1)
            
                                
    if __name__ == "__main__":
        App().mainloop()
    

    Question: Re-run frm.pack(): but this ruins the order of my main widgets.
    frm.pack_forget(), however it doesn't bring it back.

    Pack has the options before= and after. This allows to pack a widget relative to other widgets.


    Reference:

    • -before

      Use its master as the master for the slaves, and insert the slaves just before other in the packing order.


    Example using before= and self.lbl3 as anchor. The Frame are removed using .pack_forget() if no children and get repacked at the same place in the packing order.

    Note: I show only the relevant parts!

    
    class App(tk.Tk):
        def __init__(self):
            ...
            self.frm = frm = tk.Frame(self, bg='black')
            frm.pack()
            self.lbl3 = tk.Label(self, text='Bye!')
            self.lbl3.pack()
            ...
    
        def on_add(self):
            try:
                self.frm.pack_info()
            except:
                self.frm.pack(before=self.lbl3, fill=tk.BOTH, expand=True)
    
            tk.Label(self.frm, text='My name is Foo').pack()
    
        def on_expose(self, event):
            w = event.widget
            if not w.children:
                w.pack_forget()
    

    Tested with Python: 3.5 - 'TclVersion': 8.6 'TkVersion': 8.6