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:
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()
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()
widgets = {"1": {"buttons": [...], "combos": [...]}, "2": {"buttons": [...], "combos": [...]}, ...}
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.
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()