Search code examples
pythontkinterttkoptionmenu

tkinter OptionMenu in combination with trace throws "TclError: No more menus can be allocated"


I want to write a code generator in python and asking the user to select by an ttk.OptionMenu the type of variable. Right behind this OptionMenu the user has to define the value inside a ttk.Checkbutton if it is a boolean type or an entry if it is an integer or string and define a value. The GUI looks like the following:

enter image description here

If I want to change the type to int or string the ttk.Checkbutton in behind should change to an ttk.Entry, but the program does crash because it can't allocate any more menus [_tkinter.TclError: No more menus can be allocated.]. I dont know what to do and found no solution. I am connecting my checkbutton by trace with a function that gets called all the time the optionmenu has changed.

Here is my simple coding example:

import tkinter as tk
import tkinter.ttk as ttk
from functools import partial


class MyWindow(ttk.Frame):
    def __init__(self, parent):
        ttk.Frame.__init__(self, parent)
        line = [tk.StringVar(value="bool"), tk.BooleanVar(value=True)]
        self.vars = [line]
        self.show()

    def show(self):
        for widget in self.children.values():
            widget.grid_forget()
        x = 0
        for var in self.vars:
            ttk.OptionMenu(self, var[0], var[0].get(), *["bool", "int", "string"]).grid(row=x, column=0)
            var[0].trace("w", partial(self.menu_changed, line=var))
            if var[0].get() == "bool":
                ttk.Checkbutton(self, variable=var[1].get()).grid(row=x, column=1)
            else:
                ttk.Entry(self, textvariable=var[1].get()).grid(row=x, column=1)
            x += 1
        ttk.Button(self, text="+", command=self.add, width=3).grid(row=x, column=0)
        self.place(x=10, y=10, anchor=tk.NW)

    def add(self):
        line = [tk.StringVar(value="bool"), tk.BooleanVar(value=True)]
        self.vars.append(line)
        self.show()

    def menu_changed(self, *_, line):
        if line[0].get() == "bool":
            line[1] = tk.BooleanVar(value=True)
        elif line[0].get() == "int":
            line[1] = tk.IntVar(value=0)
        elif line[0].get() == "string":
            line[1] = tk.StringVar()
        self.show()


root = tk.Tk()
window = MyWindow(root)
root.mainloop()

I hope you do have any idea what's wrong. Thanks in advance.


Solution

  • There's two major problems in your code. The first is that you keep making new widgets every time self.show() or self.menu_changed() is called, the second is that you set the tracing of the variable inside your self.show() function, which is therefore executed many times.

    I rearranged your code, only creating new widgets and setting the trace in the self.add() function. This way you don't create more widgets than necessary. The only "problem" with this is that I use a StringVar for the Checkbutton/Entry even when you have a Boolean or Integer selected, so all values are converted to strings. I also added a print button to print out all current values.

    import tkinter as tk
    import tkinter.ttk as ttk
    from functools import partial
    
    
    class MyWindow(ttk.Frame):
        def __init__(self, parent):
            ttk.Frame.__init__(self, parent)
            self.lines = []
            self.add_button = ttk.Button(self, text="+", command=self.add, width=3)
            self.print_button = ttk.Button(self, text="Print", command=self.print_values)
            self.place(x=10, y=10, anchor=tk.NW)
            self.add()
    
        def show(self):
            for widget in self.children.values():
                widget.grid_forget()
            x = 0
            for line in self.lines:
                line[2].grid(row=x, column=0)
                if line[0].get() == "bool":
                    line[3].grid(row=x, column=1)
                else:
                    line[4].grid(row=x, column=1)
                x += 1
            self.add_button.grid(row=x, column=0)
            self.print_button.grid(row=x, column=1)
    
        def add(self):
            line = [tk.StringVar(value="bool"), tk.StringVar(value="1")]
            line.append(ttk.OptionMenu(self, line[0], line[0].get(), *["bool", "int", "string"]))
            line.append(ttk.Checkbutton(self, variable=line[1]))
            line.append(ttk.Entry(self, textvariable=line[1]))
            line[0].trace("w", partial(self.menu_changed, line=line))
            self.lines.append(line)
            self.show()
    
        def menu_changed(self, *_, line):
            if line[0].get() == "bool":
                line[1].set("1")
            elif line[0].get() == "int":
                line[1].set("0")
            elif line[0].get() == "string":
                line[1].set("")
            self.show()
    
        def print_values(self):
            print("Current values:")
            for line in self.lines:
                print(line[1].get())
    
    
    root = tk.Tk()
    window = MyWindow(root)
    root.mainloop()
    

    By the way, you don't really need to trace the StringVar, you can add a command parameter to the OptionMenu like:

    ttk.OptionMenu(self, line[0], line[0].get(), *["bool", "int", "string"], command=partial(self.menu_changed, line=line))