Home > Mobile >  Tkinter: access specifc widgets created with for loop
Tkinter: access specifc widgets created with for loop

Time:04-18

In my tkinter project I've created an Entry, a Label and two Buttons for each of the line in my line list using a for loop. I've also saved them in a list when created.

Right now my problem is how can I access them? For example: if the edit button of the 12th line is clicked, then I want to be able to get the entry values of the 12th entry or if my user click the delete button of the 3rd line in the list, then, only the entry, the label and the two button of the selected line should be deleted.

This is my code:

self.line_loop = []

for line in self.line_list:
    self.row_count  = 1

    self.n_entry = tk.Entry(self.list_frame, justify="center", width=4)
    self.n_entry.grid(row=self.row_count, column=0, pady=10, padx=10, ipady=3)

    self.text_label = tk.Label(self.list_frame, text=line, anchor="w", justify="left", 
                               wraplengt=701)
    self.text_label.grid(row=self.row_count, column=1, pady=10, padx=10, sticky="w")

    self.edit_button = tk.Button(self.list_frame, text="Edit", command=self.edit)
    self.edit_button.grid(row=self.row_count, column=2, pady=10, padx=10)

    self.delete_button = tk.Button(self.list_frame, text="Delete", command=self.edit)
    self.delete_button.grid(row=self.row_count, column=3, pady=10, padx=10)

    self.line_loop.append(self.n_entry)
    self.line_loop.append(self.text_label)
    self.line_loop.append(self.edit_button)
    self.line_loop.append(self.delete_button)

EDIT: This are examples of the functions. The code should work only on the clicked button and linked widgets

def delete(self):
    self.n_entry.destroy() 
    self.text_label.destroy() 
    self.edit_button.destroy() 
    self.delete_button.destroy() 
def edit(self): 
    for entry in self.line_loop:
       print(entry.get())

How can I do so?

CodePudding user response:

You don't give any details about your edit or delete functions, but I'm guessing that if you get the corresponding button to provide its index to that function, the function could index into self.line_list:

        self.line_loop = []

        for index, line in enumerate(self.line_list):
            self.row_count  = 1

            n_entry = tk.Entry(self.list_frame, justify="center", width=4)
            n_entry.grid(row=self.row_count, column=0, pady=10, padx=10, ipady=3)

            text_label = tk.Label(self.list_frame, text=line, anchor="w", justify="left", wraplengt=701)
            text_label.grid(row=self.row_count, column=1, pady=10, padx=10, sticky="w")

            edit_button = tk.Button(self.list_frame, text="Edit", command=lambda x=index: self.edit(x))
            edit_button.grid(row=self.row_count, column=2, pady=10, padx=10)

            delete_button = tk.Button(self.list_frame, text="Delete", command=lambda x=index: self.delete(x))
            delete_button.grid(row=self.row_count, column=3, pady=10, padx=10)

            self.line_loop.append((n_entry, text_label, edit_button, delete_button))
    def edit(self, index):
        n_entry, text_label, edit_button, delete_button = self.line_loop[index]
        line = self.line_list[index]
        ...

I've used enumerate() to provide indexes into self.line_list and used the command=lambda idiom to provide an index parameter to the example edit() method.

CodePudding user response:

You can also nametowidget to access individual buttons.

from tkinter import *

root=Tk()
for i in range(10):
    a=Entry(name=str(i))
    a.pack()
    root.nametowidget(f'.{str(i)}')
    Button(text='get', command=lambda temp=i:edit(temp)).pack()
def edit(num):
    Label(text=root.nametowidget(f'.{num}').get()).pack()
root.mainloop()

You can use nametowidget to create widget by name and the same to get widgets refrence.

CodePudding user response:

Your question is a little sketchy on details, but here's how to do what you want. It takes advantage of the fact that you're using the tkinter grid geometry manager, which keeps track of what row and column location of all the widgets under its control. This means you can get the information you need from it instead of keeping track of it yourself. i.e. There's no need for something like the self.line_loop list and many of the other instance attributes you're creating manually (although generally speaking that's also a viable approach if done properly).

When creating widgets in a loop it's important to make sure that the value of any variables being passed to callback functions aren't in variables that change each iteration of the loop because it doesn't work (see tkinter creating buttons in for loop passing command arguments).

A common way to avoid the problem is to use a lambda function with arguments that have been given default values as a way to "capture" the loop variable's current value. In this case it also required setting the Button widget's options in two steps in order to pass the button itself to the associated command= callback function.

Here's a runnable example based on the code in your question of doing that:

import tkinter as tk

class ListEditor:
    def __init__(self, list_frame, lines):
        self.list_frame = list_frame
        self.line_list = lines.splitlines()
        self.create_widgets()

    def create_widgets(self):
        for row, line in enumerate(self.line_list):
            entry = tk.Entry(self.list_frame, justify="center", width=4)
            entry.grid(row=row, column=0, pady=10, padx=10, ipady=3)

            text_label = tk.Label(self.list_frame, text=line, anchor="w",
                                  justify="left", wraplength=701)
            text_label.grid(row=row, column=1, pady=10, padx=10, sticky="w")

            edit_button = tk.Button(self.list_frame, text="Edit")
            edit_button.config(command=lambda widget=edit_button: self.edit(widget))
            edit_button.grid(row=row, column=2, pady=10, padx=10)

            delete_button = tk.Button(self.list_frame, text="Delete")
            delete_button.config(command=lambda widget=delete_button: self.delete(widget))
            delete_button.grid(row=row, column=3, pady=10, padx=10)

    def _get_widgets_on_same_row(self, widget):
        """Return list of all widgets on same row as given one in column order."""
        row = widget.grid_info()['row']
        return sorted(widget.master.grid_slaves(row=row),
                      key=lambda w: w.grid_info()['column'])

    def delete(self, widget):
        row = widget.grid_info()['row']
        widgets = self._get_widgets_on_same_row(widget)
        for widget in widgets:  # Get rid of associated widgets.
            widget.grid_remove()
        self.line_list[row] = None  # Indicate line was deleted.

    def edit(self, widget):
        row = widget.grid_info()['row']
        entry,text_label,edit_button,delete_button = self._get_widgets_on_same_row(widget)
        pass  # Whatever you want to do with the linked widgets (and instance data)...


if __name__ == '__main__':
    from textwrap import dedent

    lines = dedent('''\
        Lorem ipsum dolor sit amet, consectetur adipiscing elit.
        Vivamus et leo id felis faucibus varius quis et risus.
        Ut nec felis ut enim tincidunt maximus.
        Sed at nunc eleifend, vestibulum dolor nec, dictum tellus.
        Aliquam et lorem tincidunt, rhoncus libero sed, molestie massa.
        Duis quis nunc maximus, semper justo placerat, posuere turpis.
    ''')

    root = tk.Tk()
    list_frame = tk.Frame(root)
    list_frame.pack()
    editor = ListEditor(list_frame, lines)
    root.mainloop()
  • Related