I am writing mass spectrometry data reduction software and want to give my users options between different fitting styles to the data. My problem is that the radio box selection always returns the default value, rather than whatever is selected at the time the "Confirm" button is pressed and the program moves onto the next window.
I am going crazy trying to find the hidden difference between my minimal reproducible example, which works:
import tkinter as tk
def sequence_selector(sequence_data, all_data=None):
selected_sequences = [] # Initialize as empty list outside of confirm_selection
def confirm_selection():
nonlocal selected_sequences
selected_sequences[:] = [i + 1 for i in listbox.curselection()] # Modify the list in place
root.quit()
# --- GUI Setup ---
root = tk.Tk()
listbox = tk.Listbox(root, selectmode='multiple')
for seq in sequence_data:
listbox.insert(tk.END, f"Sequence {seq}")
listbox.pack()
fit_type_var = tk.StringVar(value='Linear')
for ft in ['Average', 'Linear', 'Double exponential']:
tk.Radiobutton(root, text=ft, variable=fit_type_var, value=ft).pack()
tk.Button(root, text="Reduce", command=confirm_selection).pack()
root.mainloop()
return selected_sequences, fit_type_var.get()
# --- Example Data ---
sequence_data = [1, 2, 3]
# --- Main Function ---
selected_sequences, selected_fit_type = sequence_selector(sequence_data)
print("Selected Sequences (outside sequence_selector):", selected_sequences)
print("Selected Fit Type (outside sequence_selector):", selected_fit_type)
and my code, which always returns the default value assigned to fit_type_var and has multiple options selected at start-up:
def sequence_selector(sequence_data, all_data):
# other irrelevant functions
# Confirm button
def confirm_selection():
nonlocal selected_sequences
selected_sequences = [sequence_data[len(sequence_data) - i - 1]['sequence_number'] for i in listbox.curselection()]
selected_fit_type = fit_type_var.get()
if not selected_sequences:
message = ('Please select a sequence to reduce.\nClick on a sequence in the list to select it.')
messagebox.showerror('Error', message)
return
print('selected fit type', selected_fit_type)
root.destroy()
selected_sequences = None
# Tkinter GUI
root = tk.Tk()
root.geometry("600x300")
# initialize the main frame for the sequence selector
main_frame = tk.Frame(root)
main_frame.pack(fill="both", expand=True)
# other unrelated code...
"""
right-hand frame for fit-type options
"""
right_frame = tk.Frame(main_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.Y, pady=(10,0))
fit_type_var = tk.StringVar(value='Linear')
tk.Label(right_frame, text="Choose a raw data fit type:", font=('TkDefaultFont', 10, 'bold')).pack(anchor='w')
for fit_type in ['Average', 'Linear', 'Double exponential']:
rb = tk.Radiobutton(
right_frame, text=fit_type, variable=fit_type_var, value=fit_type, anchor='w'
)
rb.pack(fill=tk.BOTH)
# reduce/confirm selection button
reduce_button = tk.Button(root, text="Reduce selected sequence(s)", command=confirm_selection,
font=('TkDefaultFont', 14),
relief=tk.RAISED, bd=4
)
reduce_button.pack(fill=tk.BOTH, expand=True)
root.mainloop()
return selected_sequences, fit_type_var.get()
The full sequence_selector function is here.
Any help would be greatly appreciated!
The issue was the upstream use of root.after(0, lambda: root.destroy())
instead of root.destroy()
in an upstream script. Here's a minimal reproducible example of code with the error:
import tkinter as tk
def sequence_selector(sequence_data):
# Confirm button
def confirm_selection():
nonlocal selected_sequences
selected_sequences = [sequence_data[len(sequence_data) - i - 1]['sequence_number'] for i in listbox.curselection()]
root.destroy()
selected_sequences = None
# Tkinter GUI
root = tk.Tk()
root.geometry("600x300")
# initialize the main frame for the sequence selector
main_frame = tk.Frame(root)
main_frame.pack(fill="both", expand=True)
# Listbox for sequence selection
listbox = tk.Listbox(main_frame, selectmode='multiple', width=25)
for item in reversed(sequence_data):
listbox.insert(tk.END, f"Sequence")
listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
center_frame = tk.Frame(main_frame, width=180)
center_frame.pack(side=tk.LEFT, fill=tk.Y, padx=(10,0))
center_frame.pack_propagate(False)
"""
right-hand frame for fit-type options
"""
right_frame = tk.Frame(main_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.Y, pady=(10,0))
fit_type_var = tk.StringVar()
fit_type_var.trace_add('write', lambda *args: print(f'Fit type: {fit_type_var.get()}'))
tk.Label(right_frame, text="Choose a raw data fit type:", font=('TkDefaultFont', 10, 'bold')).pack(anchor='w')
rb1 = tk.Radiobutton(right_frame, text="Average", variable=fit_type_var, value="Average")
rb1.pack(anchor='w')
rb2 = tk.Radiobutton(right_frame, text="Linear", variable=fit_type_var, value="Linear")
rb2.pack(anchor='w')
rb3 = tk.Radiobutton(right_frame, text="Double exponential", variable=fit_type_var, value="Double exponential")
rb3.pack(anchor='w')
rb2.select()
# reduce/confirm selection button
reduce_button = tk.Button(root, text="Reduce selected sequence(s)", command=confirm_selection,
font=('TkDefaultFont', 14),
relief=tk.RAISED, bd=4
)
reduce_button.pack(fill=tk.BOTH, expand=True)
root.mainloop() # start the GUI
print(fit_type_var.get())
return selected_sequences, fit_type_var.get()
def get_raw_data():
root=tk.Tk()
root.title('test')
sequence_data = [
{'sequence_number': 1, 'data': '1'},
{'sequence_number': 2, 'data': '4'},
]
root.after(0, lambda: root.destroy())
return sequence_data
sequence_data = get_raw_data()
sequence_selector(sequence_data)
Replacing root.after(0, lambda: root.destroy())
with root.destroy()
toggles the functionality of the radiobutton box.
Don't ask me why, but it does. Maybe someone else can explain.