-1

I have the code below which is based on this question here: Expandable and contracting frame in Tkinter

from tkinter import *
from tkinter import ttk

class Example:
    def __init__(self, root):
        list_1 = ['item_1', 'item_2']
        list_2 = ['a', 'b', 'c']

        for item in list_1 :
            frame = Frame(root, width=500, height=22)
            frame.pack(side=TOP, anchor=W)
            frame.propagate(0)

            Label(frame, text=item).pack(side=LEFT, anchor=W)
            entry = Entry(frame, width=11)
            entry.pack(side=RIGHT, pady=2, anchor=E)

            var = IntVar()
            var.set(0)

            sub_frame = Frame(root, relief="sunken", width=400, height=22, borderwidth=1)

            toggle_btn = ttk.Checkbutton(frame, width=2, text='+', command=lambda: self.toggle(var, toggle_btn, sub_frame),
                                     variable=var, style='Toolbutton')
            toggle_btn.pack(side=LEFT, anchor=E)

            for item in list_2:
                Label(sub_frame, text=item).pack(side=TOP)

    def toggle(self, show, toggle_button, sub_frame):
        if bool(show.get()):
            sub_frame.pack(fill="x", expand=1)
            sub_frame.propagate(1)
            toggle_button.configure(text='-')
        else:
            sub_frame.forget()
            toggle_button.configure(text='+')

def main():
    root = Tk()
    open = Example(root)
    root.mainloop()

if __name__ == '__main__':
    main()

I want here to create two expandable frames from list_1 and fill each with labels from list_2. The two frames should then be expandable and collapsible to show and hide the items from list_2.

The second frame functions correctly, but the first doesn't because it was overwritten by the second frame.

So the code initially gives you this:

enter image description here

If you click the '+' next to item_1, nothing happens.

If you click the '+' next to item_2, this happens.

enter image description here

By clicking on the '+' next to item_1, I want the sub_frame to be shown underneath item_1(between item_1 and item_2), just like clicking on the '+' next to item_2 shows the sub_frame underneath it.

Any help on how to get both frames to work correctly? I've read about storing the frames created in a list, however I couldn't get this to work for me.

Many thanks

blountdj
  • 599
  • 1
  • 11
  • 23

2 Answers2

1

Replace:

toggle_btn = ttk.Checkbutton(frame, width=2, text='+', command=lambda: self.toggle(var, toggle_btn, sub_frame),
                                     variable=var, style='Toolbutton')

with:

toggle_btn = ttk.Checkbutton(frame, width=2, text='+',
                                     variable=var, style='Toolbutton')
toggle_btn['command'] = lambda a1=var, a2=toggle_btn, a3=sub_frame: self.toggle(a1, a2, a3)

See more on why.


The Minimal, Complete, and Verifiable example of above issue should at longest be:

from tkinter import *

class Example:
    def __init__(self, root):
        list_1 = ['item_1', 'item_2']
        list_2 = ['a', 'b', 'c']

        for item in list_1:
            frame = Frame(root)
            frame.pack()

            var = IntVar()
            sub_frame = Frame(root)
            for item in list_2:
                Label(sub_frame, text=item).pack()

            toggle_btn = Checkbutton(frame,
                command=lambda: self.toggle(var, toggle_btn, sub_frame),
                variable=var)
            toggle_btn.pack()

    def toggle(self, show, toggle_button, sub_frame):
        if bool(show.get()):
            sub_frame.pack()
        else:
            sub_frame.forget()

def main():
    root = Tk()
    open = Example(root)
    root.mainloop()

if __name__ == '__main__':
    main()

If you don't want to overwrite simply use iterable types, such as list and dict, instead of singular variables for multiple objects such as your frames and labels. Below code generates a dictionary of frames, then a dictionary of labels for each of the frames:

import tkinter as tk


if __name__ == '__main__':
    list_1 = ['item_1', 'item_2']
    list_2 = ['a', 'b', 'c']

    root = tk.Tk()

    frames = dict()

    for item in list_1:
        frames[item] = tk.Frame(root)
        frames[item].pack()
        frames[item].labels = dict()
        for text in list_2:
            frames[item].labels[text] = tk.Label(frames[item], text=text)
            frames[item].labels[text].pack()

    root.mainloop()
Nae
  • 14,209
  • 7
  • 52
  • 79
  • How is that an issue? Even if it is, I haven't time to 'solve' that. I've spent most of my time assigned to this question to create its [mcve]. – Nae Jan 24 '18 at 19:30
  • Many thanks again for your answer. Changing the checkbutton mostly work as now the sub_frame can be ticked to toggle their appearance. The remaining issue is that both sub_frames are placed at the bottom of the whole frame whenever they are shown. By ticking the first checkbox I'm looking for the sub_frame to appear in the frame directly underneath the first checkbox (Ie between the two checkboxes). Is there a way to do this? I've tried using a dict but I couldn't figure it out. Many thanks – blountdj Jan 24 '18 at 19:31
  • The easiest solution for that would be to assign them separately to subframes of their own. Check [Bryan's answer](https://stackoverflow.com/a/48430121/7032856) if you want to do things OOP, which allows much more flexibility. – Nae Jan 24 '18 at 19:33
1

The root of your problem is that the lambda is bound to the variables, but you want it bound to the values of the variables at the time that you create the lambda. This is a very, very common mistake when using lambda.

There are many questions and answers on this site related to this problem. Here are a few:


While it is possible to fix your code as written by modifying the use of lambda, I suggest that a better solution is to create a new class that represents one collapsible frame. If this new class inherits from Frame, you can use it exactly like any other widget.

With that you can eliminate the need to use lambda, and makes it much easier to encapsulate everything about a collapsible frame in one object.

For example:

class CustomFrame(tk.Frame):
    def __init__(self, parent, label, items):
        tk.Frame.__init__(self, parent)
        header = tk.Frame(self)
        self.sub_frame = tk.Frame(self, relief="sunken",
                                  width=400, height=22, borderwidth=1)
        header.pack(side="top", fill="x")
        self.sub_frame.pack(side="top", fill="both", expand=True)

        self.var = tk.IntVar(value=0)
        self.label = tk.Label(header, text=label)
        self.toggle_btn = ttk.Checkbutton(header, width=2, text="+",
                                          variable=self.var, style='Toolbutton',
                                          command=self.toggle)
        self.entry = tk.Entry(header, width=11)

        self.label.pack(side="left")
        self.toggle_btn.pack(side="left")
        self.entry.pack(side="right", pady=2, anchor="e")
        self.sub_frame.pack(side="top", fill="both", expand=True)

        for item in items:
            tk.Label(self.sub_frame, text=item).pack(side="top")

        # this sets the initial state
        self.toggle(False)

    def toggle(self, show=None):
        show = self.var.get() if show is None else show
        if show:
            self.sub_frame.pack(side="top", fill="x", expand=True)
            self.toggle_btn.configure(text='-')
        else:
            self.sub_frame.forget()
            self.toggle_btn.configure(text='+')

With that, your Example class becomes trivial, and you can create as many frames as you want:

class Example:
    def __init__(self, root):
        list_1 = ['item_1', 'item_2']
        list_2 = ['a', 'b', 'c']

        for item in list_1:
            frame = CustomFrame(root, item, list_2)
            frame.pack(side="top", fill="x")

Note: the above code assumes that tkinter and ttk are imported like this, which is slightly different than your original code:

import tkinter as tk
from tkinter import ttk
Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685