Search code examples
python-3.xtkinter

Inserting a column in TKinter using the row where the add column button is placed


I have produced a GUI where I can dynamically add new rows to the table using a button attached to the code below (there is also one to remove the row, but I don't think it's needed here).

I then have a button called "Add Condition" which shuffles the columns across by 1, and inserts a new column label and dropdown box. All of this currently works fine and can be seen in my second code block. So if I click add condition, it created Condition 1 and a dropdown box, click again condition 2 etc.

My issue is, this only works well for row 1. If I add a new row, and then click add condition, it adds condition x (label in row 0 and depending on how many conditions in row 1) and places the dropdown box in that column. What I need is for when I click the add condition button in row 2 onwards, it adds the dropdown box to the condition 1 column and then more if required. What am I missing to achieve this?

Edited to add image of what it should look like: enter image description here

Edited to add minimal working example now below:

import os
import tkinter as tk
import pandas as pd
from tkinter import Tk, filedialog, messagebox, simpledialog, StringVar, OptionMenu, Button, Label,ttk
from tkinter.filedialog import askdirectory


# Initialise counters
row_counter = 1
targ_col = 4
widgets = {}

            
# Create Dropdown boxes
def selection_changed(event):
    selection = event.widget.get()
    messagebox.showinfo(
        title="New Selection",
        message=f"Selected option: {selection}"
    )

# Button to add a new row   
def add_row():
    global row_counter
    
    # Button to dynamically add condition columns
    row_value = row_counter + 1
    widgets[f"add_condition_button_{row_counter}"] = tk.Button(root, text="Add Condition", command=lambda: add_con(row_value))
    widgets[f"add_condition_button_{row_counter}"].grid(row=row_value, column=3)
    
    # Button to remove last added condition
    widgets[f"remove_button_{row_counter}"] = tk.Button(root, text="Remove Condition" )
    widgets[f"remove_button_{row_counter}"].grid(row=row_counter+1, column=4)
    
    widgets[f"delete_row_{row_counter}"] = tk.Button(root, text="Delete Row", command=lambda r=row_counter: del_row(r))
    widgets[f"delete_row_{row_counter}"].grid(row=row_counter+1, column=5)
    
    row_counter +=1

# Create a button to delete a row
def del_row(row):
    global row_counter
    for widget in widgets.values():
        info = widget.grid_info()
        if "row" in info and info["row"] == row + 1:
            widget.grid_forget()
    row_counter -= 1

# Create a button to add a condition dropdown menu
def add_con(row):
    global targ_col
    for widget in widgets.values():
        info = widget.grid_info()
        if info["column"] >= targ_col:
            widget.grid(column=info["column"] + 2)
    
    newcond_label=tk.Label(root, text = [F"Condition {targ_col-3}"])
    newcond_label.grid(row=0,column=targ_col)
    
    widgets[f"con_drop_{row_counter}"] = ttk.Combobox(
        state="readonly",
        values=["test","test","test"]
    )
    widgets[f"con_drop_{row_counter}"].bind("<<ComboboxSelected>>", selection_changed)
    widgets[f"con_drop_{row_counter}"].place(x=50,y=50)
    widgets[f"con_drop_{row_counter}"].grid(row=row,column=targ_col)
    
    targ_col += 1
    root.update_idletasks()
    

# TKinter Root
root = tk.Tk()
root.config(width=800, height=600)
root.title("Data Processing Tool")

# Buttons, labels and drop downs
cond_label=tk.Label(root, text = "Condition?")
cond_label.grid(row=0,column=3)



# Button to dynamically add condition columns
widgets[f"add_condition_button_{row_counter}"] = tk.Button(root, text="Add Condition", command=lambda row = row_counter: add_con(row))
widgets[f"add_condition_button_{row_counter}"].grid(row=1, column=3)


# Button to remove last added condition
widgets[f"remove_button_{row_counter}"] = tk.Button(root, text="Remove Condition" )
widgets[f"remove_button_{row_counter}"].grid(row=1, column=4)

# Button to duplicate row 1
widgets[f"addrow_button_{row_counter}"] = tk.Button(root, text="Add Row", command=add_row)
widgets[f"addrow_button_{row_counter}"].grid(row=1, column=5)


root.mainloop()

Solution

  • Issues

    There are several problems with the code.

    In the add_con function, you iterate over all widgets, regardless of whether that widget belongs to the row being processed or not. So, the wrong buttons move in the grid.

    The targ_col variable creates unnecessary columns for the next rows.

    If multiple comboboxes are created in the same row, you only store the last combobox in the dictionary (widgets[f"con_drop_{row_counter}"] use the same row_counter).

    If we add print() to track loop iterations, we will see these issues.

    def add_con(row):
        global targ_col
        print("\nROW:", row, ",", "targ_col:", targ_col)
        for widget in widgets.values():
            info = widget.grid_info()
            print(widget, widget["text"] if isinstance(widget, tk.Button) else "")
            print("BEFORE:", widget.grid_info())
            if info["column"] >= targ_col:
                widget.grid(column=info["column"] + 2)
            print("AFTER:", widget.grid_info())
        newcond_label=tk.Label(root, text = [F"Condition {targ_col-3}"])
        newcond_label.grid(row=0,column=targ_col)
        
        widgets[f"con_drop_{row_counter}"] = ttk.Combobox(
            state="readonly",
            values=["test","test","test"]
        )
        widgets[f"con_drop_{row_counter}"].bind("<<ComboboxSelected>>", selection_changed)
        #widgets[f"con_drop_{row_counter}"].place(x=50,y=50)
        widgets[f"con_drop_{row_counter}"].grid(row=row,column=targ_col)
        print("COMBO ADDED:", widgets[f"con_drop_{row_counter}"].grid_info())
        targ_col += 1
        root.update_idletasks()
    

    tkinter_insert_column_grid

    Proposed changes

    • Create a widget dictionary, where the keys are the row numbers to store and access the widgets.

    widgets = {"1": {"buttons": [...], "combos": [...]}, "2": {"buttons": [...], "combos": [...]}, ...}

    • Calculate the position of the comboboxes based on the number of comboboxes present in the row, not based on the value of a global variable.

    The rows follow a certain pattern. The first button ("Add Condition") always remains in place. The next in row is an arbitrary number of conditions (comboboxes). The next button ("Remove Condition") moves depending on the number of conditions before it. Is moves +1 column each time we add a condition to a row. The last button ("Add Row", "Delete Row") stacked to the right side of the window according to your layout. This is always the last column in the grid.

    The following solution allows you to insert any number of comboboxes after the first button, but you can set a limit on the number of conditions for the rows.

    Full code

    import tkinter as tk
    from tkinter import messagebox, ttk
    
    
    # initialise counter
    row_counter = 1
    # widgets = {"1": {"buttons": [], "combos": []}, ...}
    widgets = {}
    # optional
    max_conditions = 2
    
    
    def selection_changed(event):
        selection = event.widget.get()
        messagebox.showinfo(
            title="New Selection",
            message=f"Selected option: {selection}"
        )
    
        
    def remove_con(row):
        if not widgets[f"{row}"]["combos"]:
            return
        
        # remove last added condition
        widgets[f"{row}"]["combos"][-1].destroy()
        del widgets[f"{row}"]["combos"][-1]
        
        # move "Remove Condition" button
        widgets[f"{row}"]["buttons"][1].grid(column=widgets[f"{row}"]["buttons"][1].grid_info()["column"]-1)
    
        # move "Add Row", "Delete Row" buttons
        max_col = root.grid_size()[0]
        last_buttons = [widgets[k]["buttons"][2] for k in widgets]
        for b in last_buttons:
            if b.grid_info()["column"] != max_col:
                b.grid(column=max_col)
                
        # if necessary, remove the last added label
        max_combos = max([len(widgets[k]["combos"]) for k in widgets])
        labels = root.grid_slaves(row=0)
        if max_combos < len(labels)-1:
            labels[0].destroy()
    
     
    def add_row():
        global row_counter
        row_counter +=1
        
        # Button to dynamically add condition columns
        b = tk.Button(root, text="Add Condition", command=lambda r=row_counter: add_con(r))
        b.grid(row=row_counter, column=0)
        
        # Button to remove last added condition
        b1 = tk.Button(root, text="Remove Condition", command=lambda r=row_counter: remove_con(r))
        b1.grid(row=row_counter, column=1)
        
        b2 = tk.Button(root, text="Delete Row", command=lambda r=row_counter: del_row(r))
        b2.grid(row=row_counter, column=root.grid_size()[0]-1)
    
        widgets[f"{row_counter}"] = {"buttons": [b, b1, b2], "combos": []}
    
    
    def del_row(row):
        for w in widgets[f"{row}"]["buttons"] + widgets[f"{row}"]["combos"]:
            w.destroy()
        del widgets[f"{row}"]
    
        # if necessary, remove labels
        max_combos = max([len(widgets[k]["combos"]) for k in widgets])
        labels = root.grid_slaves(row=0)
        if max_combos < len(labels)-1:
            for i in range(0, (len(labels)-1)-max_combos):
                labels[i].destroy()
    
        # do not decrement the counter, dict keys must be unique
        # row_counter -= 1
    
    
    def add_con(row):
        combos = widgets[f"{row}"]["combos"]
        #if len(combos) == max_conditions:
            #return
        c = ttk.Combobox(
            root,
            state="readonly",
            values=["test","test","test"]
        )
        c.bind("<<ComboboxSelected>>", selection_changed)
        c.grid(row=row, column=1+len(combos))
        widgets[f"{row}"]["combos"].append(c)
        
        # move "Remove Condition" button
        widgets[f"{row}"]["buttons"][1].grid(column=widgets[f"{row}"]["buttons"][1].grid_info()["column"]+1)
        
        # move "Add Row", "Delete Row" buttons
        max_col = root.grid_size()[0]
        last_buttons = [widgets[k]["buttons"][2] for k in widgets]
        for b in last_buttons:
            if b.grid_info()["column"] != max_col:
                b.grid(column=max_col)
                
        # add column name
        labels = [l["text"] for l in root.grid_slaves(row=0)]
        if f"Condition {len(combos)}" in labels:
            return
        else:
            newcond_label=tk.Label(root, text=f"Condition {len(combos)}")
            newcond_label.grid(row=0, column=len(widgets[f"{row}"]["combos"]))
        
    
    # TKinter Root
    root = tk.Tk()
    root.minsize(width=800, height=600)
    root.title("Data Processing Tool")
    
    # Buttons, labels and drop downs
    cond_label=tk.Label(root, text = "Condition?")
    cond_label.grid(row=0, column=0)
    
    # Button to dynamically add condition columns
    b = tk.Button(root, text="Add Condition", command=lambda row=row_counter: add_con(row))
    b.grid(row=1, column=0)
    
    
    # Button to remove last added condition
    b1 = tk.Button(root, text="Remove Condition", command=lambda row=row_counter: remove_con(row))
    b1.grid(row=1, column=1)
    
    # Button to duplicate row 1
    b2 = tk.Button(root, text="Add Row", command=add_row)
    b2.grid(row=1, column=2)
    
    # set first row
    widgets[f"{row_counter}"] = {"buttons": [b, b1, b2], "combos": []}
    
    # print(root.grid_size()) # (3, 2) 0,1,2 x 0,1
    
    root.mainloop()