Search code examples
pythontkinter

Python/tkinter radio buttons always return default value, displaying multiple options


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!


Solution

  • 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.