python-3.xtkinterlambdacombobox

How to call a function from a function in a class that is called using a button with a lambda in Python3x?


I have a class: MyClassName for example, with two functions: one to calculate a number and one to validate if a group of comboboxes (tkinter) all have selections. I'm also using tkinter for a button to call the calculate function using a lambda because I want the user to make the combobox selections and then press the calculate button, hence the need for a lambda.

Here is the button code:

# Calculate Button
        self.calculate_button = ttk.Button(button_frame,  
                            command = lambda: calculate(self),  # Use a lambda to send params to the function for command.
                            width = 12, 
                            text = "CALCULATE",
                            bootstyle = 'primary', 
                            state = 'enabled') 
# In the calculate function, I call another function:
def calculate(self):
            # Call the Class function from within another Class function.
            self.validate_Comboboxes() ...

And in the validate_Comboboxes function I use something like this:

 # Function to validate if all the Comboboxes were selected.
        def validate_Comboboxes(self):
           for widget in self.base_frame.winfo_children():
                if isinstance(widget, ttk.Combobox) and widget.get() == "":
                    messagebox.showerror("error", "All Comboboxes for the base frame must be selected for calculations!")
                    break
                    ...

I tested the validate_Comboboxes function standalone outside this app, so I know it works. However, I get this error: AttributeError: 'MyClassName' object has no attribute 'validate_Comboboxes'. Which seems like I don't have it declared properly? I understand if it was simply a class attribute, you would use: myAttribute(self) to declare it, but validate_Comboboxes(self) doesn't make sense to me as I declare it using my def statement, unless I"m missing something?

What is the correct way to code this for my use case, mainly to validate the user has all the comboboxes selected before they hit the calculate button?

I thought if you had two functions inside a class, like functionA(self) and functionB(self), you could call functionA from functionB using self.function(). So is the mechanism of using a button with a lambda the issue here?


Solution

  • Using your previous questions code as context, you are experiencing these errors due to the scope at which you define your validateComboboxes, calculate, and clear functions.

    Considering the following to be a summarized version of your code...

    class ComboBoxTestGroup:
        def __init__(self, master):
            ...
            ...
            
            def calculate(self):
                ...
            def clear(self):
                ...
            def validateComboboxes(self):
                ...
    

    Because the three functions previously mentioned are defined inside the __init__ method body, those functions are not bound to the ComboBoxTestGroup instance and are only accessible to the code that executes within the __init__ methods scope. This, plus the fact that you are using a lambda which will be evaluated during runtime means that when you attempt to call the calculate(self) the function definition will no longer exist.

    There is a very simple solution. Define your methods in the body/scope of the class instead of the __init__ method. This will ensure that the functions will be bound to the class instance and will remain callable for the lifetime of the instance object.

    Example:

    import tkinter
    from tkinter import *
    from tkinter import messagebox
    from tkinter.ttk import Combobox
    import ttkbootstrap as ttk
    from ttkbootstrap.constants import *
    
    window = ttk.Window(themename = 'superhero')
    window.title('Combobox Test Group')
    window.geometry('1200x275')
    
    class ComboBoxTestGroup:
        def __init__(self, master):
    
            # Create the main frame.
            self.main_frame = ttk.Frame(master, relief=SUNKEN, borderwidth=1)
            self.main_frame.grid(row=0, column=0)
    
            # Dropdowns
            self.base_frame = tkinter.LabelFrame(self.main_frame, text='BASE', relief=SUNKEN, borderwidth=1)
            self.base_frame.grid(row=4, column=0, sticky="news", padx=10, pady=15)
    
            self.base_label_ti = ttk.Label(self.base_frame, text='Technical Interface (TI)', font=('Helvetica', 10))
            self.base_label_ti.grid(row=0, column=0)
    
            self.ti_values = [("High", "H", "1.0"), ("Medium", "M", "0.5"), ("Low", "L", "0.1"),  ("None", "N", "0.0")]
            self.ti_combobox = ttk.Combobox(self.base_frame, bootstyle='primary', values=self.ti_values, state = 'readonly')
            self.ti_combobox.grid(row=1, column=0)
    
            self.base_label_ap = ttk.Label(self.base_frame, text='Application Project (AP)', font=('Helvetica', 10))
            self.base_label_ap.grid(row=0, column=1)
    
            self.ap_values = [("High", "H", "1.0"), ("Medium", "M", "0.5"), ("Low", "L", "0.1"),  ("None", "N", "0.0")]
            self.ap_combobox = ttk.Combobox(self.base_frame, bootstyle='primary', values=self.ap_values, state = 'readonly')
            self.ap_combobox.grid(row=1, column=1)
    
            self.base_label_al = ttk.Label(self.base_frame, text='Application Logs (AL)', font=('Helvetica', 10))
            self.base_label_al.grid(row=0, column=2)
    
            self.al_values = [("High", "H", "1.0"), ("Medium", "M", "0.5"), ("Low", "L", "0.1"),  ("None", "N", "0.0")]
            self.al_combobox = ttk.Combobox(self.base_frame, bootstyle='primary', values=self.al_values, state = 'readonly')
            self.al_combobox.grid(row=1, column=2)
    
            self.base_label_ic = ttk.Label(self.base_frame, text='Internal Composition (IC)', font=('Helvetica', 10))
            self.base_label_ic.grid(row=0, column=3)
    
            self.ic_values = [("High", "H", "1.0"), ("Medium", "M", "0.5"), ("Low", "L", "0.1"),  ("None", "N", "0.0")]
            self.ic_combobox = ttk.Combobox(self.base_frame, bootstyle='primary', values=self.ic_values, state = 'readonly')
            self.ic_combobox.grid(row=1, column=3)
    
            self.base_label_fc = ttk.Label(self.base_frame, text='Fixed Controls (FC)', font=('Helvetica', 10))
            self.base_label_fc.grid(row=0, column=4)
    
            self.fc_values = [("High", "H", "1.0"), ("Medium", "M", "0.5"), ("Low", "L", "0.1"),  ("None", "N", "0.0")]
            self.fc_combobox = ttk.Combobox(self.base_frame, bootstyle='primary', values=self.fc_values, state = 'readonly')
            self.fc_combobox.grid(row=1, column=4)
    
            # For loop to space widgets within the base_frame
            for widget in self.base_frame.winfo_children():
                widget.grid_configure(padx=20, pady=10)
    
            # Create a frame for the buttons.
            button_frame = ttk.Frame(self.main_frame, relief=SUNKEN, borderwidth=1)
            button_frame.grid(row=5, column=0)
    
            # Validate Button
            self.calculate_button = ttk.Button(button_frame,
                                command = self.calculate,  # no need for lambda
                                width = 12,
                                text = "VALIDATE",
                                bootstyle = 'primary',
                                state = 'enabled')
    
            self.calculate_button.grid(row=10, column=0, padx=75, pady=10)
    
            # Clear Button
            self.clear_button = ttk.Button(button_frame,
                                command=self.clear,  # no need for lambda
                                width = 12,
                                text = "CLEAR",
                                bootstyle = 'primary',
                                state = 'enabled')
            self.clear_button.grid(row=10, column=1, padx=75, pady=10)
    
        def calculate(self):
            self.validateComboboxes()
    
        # Function to clear the ttk widgets.
        def clear(self):
            # This clears the Comboboxes by the enclosing frame.
            for widget in self.base_frame.winfo_children():
                if isinstance(widget, ttk.Combobox):
                    widget.set("")
    
                    # Function to validate if all the Comboboxes were selected.
        def validateComboboxes(self):
            for widget in self.base_frame.winfo_children():
                if isinstance(widget, ttk.Combobox) and widget.get() == "":
                    messagebox.showerror("error", "All Comboboxes must be selected in order to do calculations!")
    
    widget_class = ComboBoxTestGroup(window)
    
    # Run the GUI
    window.mainloop()