1

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.

stovfl
  • 14,998
  • 7
  • 24
  • 51
r.ook
  • 13,466
  • 2
  • 22
  • 39
  • @stovfl thanks for the suggestion, but this is just my [mre]. My real examples have a bunch of varying widgets within the `frm` that requires more controls. Some are `Label`s, `LabelFrame`s, `Button`s... etc... – r.ook Jan 03 '20 at 20:36
  • The code in your question is **not** a [mre]. – martineau Jan 03 '20 at 20:48
  • @martineau Could you please clarify? The code in my question produces the result I'm experiencing that I want to change. If you mean I should include all my other widgets in there it'd hardly be a minimal example? – r.ook Jan 03 '20 at 20:59
  • ***"the same height required by the Label"***: Count it by yourself, looping `frm.children ; .winfo_height()` – stovfl Jan 03 '20 at 21:03
  • @stovfl after `lbl2.destroy()`, there's nothing in `frm.children` though. I understand that by creating `lbl2` within `frm` it expanded the size, but I'm wondering if there's a built-in method I'm overlooking that undoes it. – r.ook Jan 03 '20 at 21:05
  • 1
    @r.ook ***"after `lbl2.destroy()`, there's nothing "***: Yes in your example it's nothing which results in `height == 0`. But you claim you have other widgets which get in count. – stovfl Jan 03 '20 at 21:07
  • My other widgets are in my real codebase, which in the interest of minimal representation, I didn't include here. But I digress - even with this example, after `lbl2` is removed, the height is not `0`, and therefore the `frm` doesn't shrink back. There's nothing else in the `frm` that should take up height, and yet it won't shrink. – r.ook Jan 03 '20 at 21:16
  • r.ook: I meant it's incomplete in the sense that it cannot be run standalone to reproduce the problem. – martineau Jan 03 '20 at 21:56

2 Answers2

4

When you destroy the last widget within a frame, the frame size is no longer managed by pack or grid. Therefore, neither pack nor grid knows it is supposed to shrink the frame.

A simple workaround is to add a small 1 pixel by 1 pixel window in the frame so that pack still thinks it is responsible for the size of the frame.

Here's an example based off of the code in the question:

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

def delete_the_label():
    lbl2.destroy()
    if len(frm.winfo_children()) == 0:
        tmp = tk.Frame(frm, width=1, height=1, borderwidth=0, highlightthickness=0)
        tmp.pack()
        root.update_idletasks()
        tmp.destroy()

button = tk.Button(root, text="Delete the label", command=delete_the_label)
button.pack()


root.mainloop()
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
3

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

Community
  • 1
  • 1
stovfl
  • 14,998
  • 7
  • 24
  • 51
  • This code won't run as posted - `NameError: name 'Application' is not defined`. Also, on OSX you can't put commands directly on the menubar. Otherwise, it's a cool solution. – Bryan Oakley Jan 03 '20 at 22:57
  • @BryanOakley: *code won't run*. Fixed, `import ...` also missing. * put commands directly on the menubar*: How does it look on OSX? – stovfl Jan 03 '20 at 23:08
  • On OSX the commands simply don't show up at all. OSX silently ignores them. All commands must be on dropdown menus. – Bryan Oakley Jan 04 '20 at 00:46