Search code examples
pythonpython-3.xtkinterttkttkwidgets

How do I change a ttk Combobox values that was made in a for loop


I have this sample code I am running. I am creating a window, and when I load the template page and click the "Click me" button, it adds 20 boxes on the screen. 10 rows, 2 wide. Column 1 is Car makes, and column 2 is Models.

When I click the Make box in row 1, and change it from Ford to Toyota, I want the model combobox in row 1 to change to show the Toyota models. But it only works for the last row. Is it possible to get each row to work?

import tkinter as tk
from tkinter import font as tkfont, filedialog, messagebox
from tkinter.ttk import Combobox

class SLS_v1(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)

        # Setting up the root window
        self.title('Test App')
        self.geometry("200x300")
        self.resizable(False, False)

        self.title_font = tkfont.Font(family='Helvetica', size=18, weight="bold", slant="italic")

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.frames = {}
        self.frames["MenuPage"] = MenuPage(parent=container, controller=self)
        self.frames["template"] = template(parent=container, controller=self)

        self.frames["MenuPage"].grid(row=0, column=0, sticky="nsew")
        self.frames["template"].grid(row=0, column=0, sticky="nsew")
        self.show_frame("MenuPage")

    def show_frame(self, page_name):
        frame = self.frames[page_name]
        frame.tkraise()


class MenuPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller

        template = tk.Button(self, text='Template', height=3, width=20, bg='white', font=('12'),
                                command=lambda: controller.show_frame('template'))
        template.pack(pady=50)


class template(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.controller = controller
        self.grid(columnspan=10, rowspan=10)

        button = tk.Button(self, text='Click me', command= lambda: stored_functions().add_boxes(self))
        button.grid(row=11, column=1)


class stored_functions():

    make = ['Ford', 'Toyota', 'Honda']
    models = [['F-150', 'Mustang', 'Explorer'], ['Camry', 'Corolla', 'Tacoma'], ['Civic', 'CRV', 'Accord']]

    def add_boxes(self, main_window):
        for row in range(10):
            self.make_var = tk.StringVar(main_window)
            self.make_options = self.make
            self.make_var.set(self.make_options[0])
            self.make_selection = tk.ttk.Combobox(main_window, value=self.make_options,
                                                     state='readonly', width=10)
            self.make_selection.current(0)
            self.make_selection.bind('<<ComboboxSelected>>', lambda event:
                                                    stored_functions.update_models(self, selection=self.make_selection))
            self.make_selection.grid(row=row, column=1)

            self.model_var = tk.StringVar(main_window)
            self.model_options = self.models[0]
            self.model_var.set(self.model_options[0])
            self.model_selection = tk.ttk.Combobox(main_window, value=self.model_options,
                                                     state='readonly', width=10)
            self.model_selection.current(0)
            self.model_selection.grid(row=row, column=2)

    def update_models(self, selection):
        if selection.get() == 'Ford':
            self.model_options = self.models[0]
        if selection.get() == 'Toyota':
            self.model_options = self.models[1]
        if selection.get() == 'Honda':
            self.model_options = self.models[2]
        self.model_selection.config(values=self.model_options)
        self.model_selection.current(0)

if __name__ == "__main__":
    app = SLS_v1()
    app.mainloop()

Solution

  • You have used same variables for car brands and models selection, so the variables refer the last set after the for loop.

    You need to pass the model combobox to update_models() using default value of argument:

    class stored_functions():
    
        make = ['Ford', 'Toyota', 'Honda']
        models = [['F-150', 'Mustang', 'Explorer'], ['Camry', 'Corolla', 'Tacoma'], ['Civic', 'CRV', 'Accord']]
    
        def add_boxes(self, main_window):
            for row in range(10):
                self.make_var = tk.StringVar(main_window)
                self.make_options = self.make
                self.make_var.set(self.make_options[0])
                self.make_selection = tk.ttk.Combobox(main_window, value=self.make_options,
                                                         state='readonly', width=10)
                self.make_selection.current(0)
                self.make_selection.grid(row=row, column=1)
    
                self.model_var = tk.StringVar(main_window)
                self.model_options = self.models[0]
                self.model_var.set(self.model_options[0])
                self.model_selection = tk.ttk.Combobox(main_window, value=self.model_options,
                                                         state='readonly', width=10)
                self.model_selection.current(0)
                self.model_selection.grid(row=row, column=2)
    
                # pass the corresponding model combobox to bind function
                self.make_selection.bind(
                    '<<ComboboxSelected>>',
                    lambda event, peer=self.model_selection: self.update_models(event.widget.get(), peer)
                )
    
        def update_models(self, selection, model_selection):
            model_options = self.models[self.make.index(selection)]
            model_selection.config(values=model_options)
            model_selection.current(0)
    

    Note that it is not necessary to use instance variables inside the for loop. Also those StringVars are not used at all, so the functions can be simplified as below:

    class stored_functions():
    
        make = ['Ford', 'Toyota', 'Honda']
        models = [['F-150', 'Mustang', 'Explorer'], ['Camry', 'Corolla', 'Tacoma'], ['Civic', 'CRV', 'Accord']]
    
        def add_boxes(self, main_window):
            for row in range(10):
                make_selection = tk.ttk.Combobox(main_window, value=self.make,
                                                 state='readonly', width=10)
                make_selection.current(0)
                make_selection.grid(row=row, column=1)
    
                model_selection = tk.ttk.Combobox(main_window, value=self.models[0],
                                                  state='readonly', width=10)
                model_selection.current(0)
                model_selection.grid(row=row, column=2)
    
                make_selection.bind(
                    '<<ComboboxSelected>>',
                    lambda event, peer=model_selection: self.update_models(event.widget.get(), peer)
                )
    
        def update_models(self, selection, model_selection):
            model_options = self.models[self.make.index(selection)]
            model_selection.config(values=model_options)
            model_selection.current(0)