Search code examples
python-3.xtkinterdestroytkinter-entry

How to delete chosen auto-generated entry box


Edited to reflect suggestion from Paul Cornelius:

I'm struggling when trying to delete an auto generated entry box:

Each x_input has a corresponding v_input either side of it. The v_input is optional and I'd like a corresponding checkbox to be able to forget or deactivate its paired x_input

I've been trying to use an indexing method with no success.

Simplified code below, skip to PROBLEM CODE BEINGS to see the actual generation/destruction code.

######### FRAME SET-UP ##########

from tkinter import *

"""Main app frame"""
class build_app(Tk):
    def __init__(self):
        Tk.__init__(self)
        self._frame = None
        self.switch_frame(home)

    """Old frame destruction"""
    def switch_frame(self, frame_class):
        new_frame = frame_class(self)
        if self._frame is not None:
            self._frame.forget()
        self._frame = new_frame
        self._frame.pack()

"""Home frame"""
class home(Frame):
    def __init__(self, master):
        Frame.__init__(self, master)

        """Label frame definitions"""
        build_labelframe = LabelFrame(self, text="Build", padx=5, pady=5)

        """Label frame positions"""
        build_labelframe.grid(row=2, column=0, sticky=N, padx=5, pady=5)

        """Build label definitions"""
        x_label = Label(build_labelframe, text="x")

        """Build label positions"""
        x_label.grid(row=5, column=0)

        """Build entry definition"""
        count_input = Entry(build_labelframe, width=20)

        """Build entry text definition"""
        count_input.insert(0, "Enter count")
        count_input.configure(justify=CENTER)

        """Build entry position"""
        count_input.grid(row=3, column=0, padx=2)

        """create table button command"""
        def click_create_table():
            global given_count

            given_count = int(count_input.get())

########## PROBLEM CODE BEGINS ##########

            """Build entry definition/position function"""
            p = 0

            x = []
            v = []

            remove_v = []

            for i in range(given_count):
                v_input = Entry(build_labelframe, width=20)
                v_input.grid(row=6 + p + i, column=0)
                v_input.configure(justify=CENTER)
                v.append(v_input)

                x_input = Entry(build_labelframe, width=20)
                x_input.grid(row=7 + p + i, column=0)
                x_input.configure(justify=CENTER)
                x.append(x_input)

                def handle_check(n):
                    def remove_v_command():
                        v[n].grid_forget()
                    return remove_v_command

                remove_v_tick = Checkbutton(build_labelframe, text="Remove veil", bd=0, command=lambda:handle_check(remove_v.index(remove_v[i])))
                remove_v_tick.grid(row=6 + p + i, column=1)
                remove_v.append(remove_v_tick)

                p += 2

            remove_v_tick = Checkbutton(build_labelframe, text="Remove veil", bd=0, command=lambda:handle_check(len(remove_v)))
            remove_v_tick.grid(row=6 + p + i, column=1)
            remove_v.append(remove_v_tick)

            v_input = Entry(build_labelframe, width=20)
            v_input.grid(row=6 + p + i, column=0)
            v.append(v_input)

            i += 1

        """Build button definition"""
        create_table_button = Button(build_labelframe, text="Create table", width=15, command=click_create_table)

        """Build button position"""
        create_table_button.grid(row=4, column=1)

if __name__ == "__main__":
    app = build_app()
    app.mainloop()

This method sucessfully gets the correct index vlaue for the listed tickbox. However, it doesn't correctly pass the index value into the grid_forget function and the corresponding v_input box remains.


Solution

  • This is a common issue with this type of program. The problem revolves around your function remove_v_command. If only you could pass an integer variable to that function, indicating which widgets to remove from the grid, something like the following (I have made v, x, and y member variables):

    def remove_v_command(self, n):
        self.v[n].grid_forget()
        self.x[n].grid_forget()
        self.y[n].grid_forget()
    

    That would coordinate the widgets in the v, x, and y arrays so the widgets get removed together. I think that's approximately what you're trying to do. If I've misinterpreted your requirements you will have to make some changes here.

    But the event handler is called internally by tkinter, and there is no way to make tkinter pass the extra variable n. There are a couple of ways to solve this. The one I prefer is to use a Python closure, like this:

    def handle_check(self, n):
        def remove_v_command():
            self.v[n].grid_forget()
            self.x[n].grid_forget()
            self.y[n].grid_forget()
        return remove_v_command
    

    Then you also change the line of code where you create the Checkbutton, like this:

    remove_v_tick = Checkbutton(
        build_labelframe, text="Remove veil", bd=0,                           
        command=self.handle_check(len(remove_v)))
    

    The method handle_check isn't itself an event handler. It returns a function that's an event handler, and (as required by tkinter) must take zero arguments. Since a new function object is created every time you call handle_check, you end up with a different event handler for every Checkbox. These objects remember the values for self and n that were passed originally; therefore each checkbox gets linked to the most recently added Entry fields. So you now have event handlers that know which objects to remove from the grid.

    I think it's cleanest if you make handle_check a member function, and make x, y, and v member objects.

    EDIT: Here is a complete working program.

    ######### FRAME SET-UP ##########
    
    from tkinter import *
    
    """Main app frame"""
    class build_app(Tk):
        def __init__(self):
            Tk.__init__(self)
            self._frame = None
            self.switch_frame(home)
    
        """Old frame destruction"""
        def switch_frame(self, frame_class):
            new_frame = frame_class(self)
            if self._frame is not None:
                self._frame.forget()
            self._frame = new_frame
            self._frame.pack()
    
    """Home frame"""
    class home(Frame):
        def __init__(self, master):
            Frame.__init__(self, master)
    
            """Label frame definitions"""
            build_labelframe = LabelFrame(self, text="Build", padx=5, pady=5)
    
            """Label frame positions"""
            build_labelframe.grid(row=2, column=0, sticky=N, padx=5, pady=5)
    
            """Build label definitions"""
            x_label = Label(build_labelframe, text="x")
    
            """Build label positions"""
            x_label.grid(row=5, column=0)
    
            """Build entry definition"""
            count_input = Entry(build_labelframe, width=20)
    
            """Build entry text definition"""
            count_input.insert(0, "Enter count")
            count_input.configure(justify=CENTER)
    
            """Build entry position"""
            count_input.grid(row=3, column=0, padx=2)
    
            """create table button command"""
            def click_create_table():
                global given_count
    
                given_count = int(count_input.get())
    
    ########## PROBLEM CODE BEGINS ##########
    
                """Build entry definition/position function"""
                p = 0
    
                self.x = []
                self.v = []
    
                remove_v = []
    
                for i in range(given_count):
                    v_input = Entry(build_labelframe, width=20)
                    v_input.grid(row=6 + p + i, column=0)
                    v_input.configure(justify=CENTER)
                    self.v.append(v_input)
    
                    x_input = Entry(build_labelframe, width=20)
                    x_input.grid(row=7 + p + i, column=0)
                    x_input.configure(justify=CENTER)
                    self.x.append(x_input)
    
                    remove_v_tick = Checkbutton(
                        build_labelframe,
                        text="Remove veil",
                        bd=0,
                        command=self.handle_check(len(remove_v)))
                    remove_v_tick.grid(row=6 + p + i, column=1)
                    remove_v.append(remove_v_tick)
    
                    p += 2
    
                remove_v_tick = Checkbutton(
                    build_labelframe, text="Remove veil", bd=0,
                    command=self.handle_check(len(remove_v)))
                remove_v_tick.grid(row=6 + p + i, column=1)
                remove_v.append(remove_v_tick)
    
                v_input = Entry(build_labelframe, width=20)
                v_input.grid(row=6 + p + i, column=0)
                self.v.append(v_input)
    
                i += 1
    
            """Build button definition"""
            create_table_button = Button(build_labelframe,
                                         text="Create table",
                                         width=15, command=click_create_table)
    
            """Build button position"""
            create_table_button.grid(row=4, column=1)
    
        def handle_check(self, n):
            def remove_v_command():
                self.v[n].grid_forget()
            return remove_v_command
    
    app = build_app()
    app.mainloop()
             
    

    EXPLANATION:

    The calls to the Button constructor take a keyword argument of the form command=f. Here 'f' is a function object. Typical usage is something like, command=click_create_table, where "click_create_table " is the name of a function, defined somewhere with a def click_create_table(...) statement. It could also be something like, lambda n: handle_check(n) because "lambda" creates an anonymous function object.

    But it doesn't have to be exactly like that. Any expression that evaluates to a function object will work. Such an expression is self.handle_check(len(remove_v)) because self.handle_check returns a function. Functions, in Python, are just objects like anything else; they can be returned, stored in variables, passed as arguments, etc.

    Tkinter calls the function when the button is clicked. Note that tkinter isn't calling self.handle_check; it's calling the object created and returned by self.handle_check.

    The argument that is passed to self.handle_check is used as an index into the list self.v. The function returned by self.handle_check is "an object that knows which Entry field to grid_forget when the user clicks on a specific button." It does this by keeping track of the value originally passed as an argument to handle_check.

    It so happens that len(remove_v) is the right index of the Entry to be deleted. That's because the new Checkbutton has not yet been added to remove_v when handle_check is called. The first time through the loop, len(remove_v) will be 0, and self.v[0] is the Entry to be hidden when that Checkbutton is clicked. The second time through the loop, len(remove_v) is 1 and self.v[1] is the associated Entry. And so on.