简体   繁体   中英

How to make python - tkinter dropdown command update after event

I am remaking a GUI calculator app in Tkinter, in order to learn about classes, methods, attributes, and also to shorten my original code. In order to shorten the code, I made a frame class that generates frames, entries, labels and dropdown menus, so I don't have to create them individually. Everything went well until I got to the dropdown menu part. When the user selects a different option from the Filters - dropdown menu like V, or B or L etc. the value in frame 1 -> entry[1] doesn't update. The method that updates the value in that entry is called add(self) and it's a part of calculator class. Here is the simple version

import numpy as np
import tkinter as tk


window = tk.Tk()
window.geometry("920x500")
window.resizable(0,0)
window.title('Exposure Time Calculator')


class Calculator:
    
    def __init__(self, window):
        
        self.create_test_frame1()
        self.create_test_frame2()
        self.add(None)
        
    def create_test_frame1(self):
        
        labelvalues=['val 1','val 2']
        entryvalues=['203','1333']
        self.frame_1 = frame('Test Frame 1',labelvalues,entryvalues,6, 2, 0, 0, "no",0,0,0,0,0,30,40)

    def create_test_frame2(self):
        labelvalues = ['val 3','val 4']
        entryvalues = ['10','24.5']
        option_menu_values = ['B','V','R','I','Luminance','Hydrogen 3nm']
        
       
        
        self.frame_2 = frame('Frame 2', labelvalues, entryvalues, 14, 2, 0, 2, 
                                  "option_menu1_yes", option_menu_values,'Filters',
                                  0,0,0,
                                  5,20)
    
    def add(self, e):
       
       qe = self.frame_1.entry[1]
       bandOption = self.frame_2.clicked.get()
      
       if bandOption == "B":
           
           qe.delete(0,tk.END)
           qe.insert(0,22)
       elif bandOption == "V":
           qe.delete(0,tk.END)
           qe.insert(0,33)
        
    
    
class frame:
#                       Creates a frame class for automatic frame generation
#                         with entries, labels and/or option menus
# 1. name : frame name
# 2. label_default_values: name of labels
# 3. entry_default_values: default values in entries
# 4. entry_width: the entries dimensions
# 5. I: number of labels and entries
# 6. grid_row: frame grid row placement
# 7. grid_column: frame grid column placement
# 8. option_menu: true or false if user wants a option list or not
# 9. option_list_values: list for option menu
# 10. option_label: name for option menu label
# 11. ipax, ipady: padding
# 12. comand: comand for option list

    def __init__(self, name, label_default_values, entry_default_values, entry_width, I, grid_row, grid_column, 
                 option_menu1, option_list_values, option_label, 
                 option_menu2, option2_list_values,option_label2,
                 ipad_x, ipad_y
                ):
        
        self.name = name
        self.label_default_values = label_default_values
        self.entry_default_values = entry_default_values
        self.I = I
        self.grid_row = grid_row
        self.grid_column = grid_column
        self.dropMenu_options = option_list_values
        self.label = option_label
        self.entry_width = entry_width
        self.dropMenu_options2 = option2_list_values
        self.option_label2 = option_label2
        self.ipad_x = ipad_x
        self.ipad_y = ipad_y
        

        frame = tk.LabelFrame(window, text = name, highlightbackground='grey', highlightthickness=1)
        frame.grid(row=self.grid_row, column=self.grid_column, padx=5, pady=5, ipadx=ipad_x, ipady=ipad_y)
       
        if option_menu1 == "option_menu1_yes":
         
            
            self.clicked = tk.StringVar()
            self.clicked.set(self.dropMenu_options[0])
            self.drop = tk.OptionMenu(frame, self.clicked, *self.dropMenu_options, command = self.add)
            self.drop.grid(row=5, column=1, sticky="ew")
            label = tk.Label(frame, text = option_label, highlightbackground='grey', highlightthickness=1)
            label.grid(row = 5, column = 0, sticky = "w")
        
        if option_menu2 == "option_menu2_yes":
            self.clicked2 = tk.StringVar()
            self.clicked2.set(self.dropMenu_options2[0])
            self.drop2 = tk.OptionMenu(frame, self.clicked2, *self.dropMenu_options2)
            self.drop2.grid(row=6, column=1, sticky="ew")
            label = tk.Label(frame, text = option_label2, highlightbackground='grey', highlightthickness=1)
            label.grid(row = 6, column = 0, sticky = "w")
            
        
        self.entry ={}

        for i in range(0, self.I):
         
            label = tk.Label(frame, text = self.label_default_values[i], justify = "left")
            label.grid(row=i, column=0, sticky = "w")
            
            self.entry[i] = tk.Entry(frame, textvariable = float(self.entry_default_values[i]), width=self.entry_width)
            self.entry[i].grid(row=i, column=1, sticky = "e")
            self.entry[i].delete(0, tk.END)
            self.entry[i].insert(0, self.entry_default_values[i])  

c=Calculator(window)
window.mainloop()

There are 2 problems:

  • The first one is that you mentioned and to fix it:
    rename def add(self) to def add(self, e) and rename add() to add(None) . Then change lambda event: self.add to self.add
  • The second one is:
    AttributeError: 'frame' object has no attribute 'frame_camera' but is not question related

The method add is in the Calculator class, so instead of self.add you need to call add on the calculator. Since the frame doesn't know what the calculator is, you need to pass it in when constructing the frame.

Something like the following, where the calculator instance is passed as the first option:

self.frame_1 = frame(self, 'Test Frame 1', ...)

Next, you need to define your class to accept and save the reference to the calculator and then use it in the command of the OptionMenu :

class frame:
    def __init__(self, calculator, name, ...):
        self.calculator = calculator
        ...
        self.drop = tk.OptionMenu(..., command = self.calculator.add)

Also, you define add like this:

def add(self, e):

I assume that means you think the second parameter is an event object. It is not. It is the value that was picked from the optionmenu.

Arguably, a better way to define this would be to actually use this new value if provided, and fall back to calling get if a value isn't provided. Also, you can reduce the wall of if statements into a single dictionary lookup to make the code shorter and more robust.

def add(self, new_value=None):

   qe = self.frame_1.entry[1]
   bandOption = self.frame_2.clicked.get() if new_value is None else new_value

   band = {"B": 22, "V": 33}
   qe.delete(0, "end")
   qe.insert(0, band[bandOption])

This solution is 2/3 the size of your original, and more flexible and easier to maintain.

It works if I define add(event) outside classes.

def add(event):
   
   qe = c.frame_1.entry[1]
   bandOption = c.frame_2.clicked.get()
  
   if bandOption == "B":
       qe.delete(0,tk.END)
       qe.insert(0,22)
   elif bandOption == "V":
       qe.delete(0,tk.END)
       qe.insert(0,33)

And this in the frame class:

self.drop = tk.OptionMenu(frame, self.clicked, *self.dropMenu_options, command = lambda event:add(event))

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM