Tkinter: function doesn't print when option menus are dynamically linked

63 Views Asked by At

I'm trying to create 2 OptionMenus that depend on each other in a way that the selection on the first will provide different options for the second.

I was able to link those two together using trace. Both OptionMenus are linked to a command and whenever a selection is made, a message is printed on the terminal.

Although this works for the first OptionMenu, I can't get to print anything when making a selection on the second. Note that if I the two OptionMenus aren't dynamically linked to each other (i.e. if they are constructed as separate widgets) printing works great for both of them.

Any idea on what might be the issue?

Here's a code that illustrates the issue.

import tkinter as tk
from tkinter import *
from tkinter import ttk
from tkinter import messagebox

from PIL import ImageTk, Image

import os

#____________________________________________________________________________________________   
#This will be the main window
window = tk.Tk()
window.geometry('700x700')
window.title("daq")

#____________________________________________________________________________________________
#Lower frame
frame_main = Frame(window)
frame_main.place(rely=0.10, relx=0.0, relwidth=1.0)

#Create a tabcontrol
tabControl = ttk.Notebook(frame_main)
tabControl.grid(column=0, row=1)

# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#Digitizer tab
tab_Digitizer    = ttk.Frame(tabControl)
tabControl.add(tab_Digitizer, text='   Digitizer   ')
digitizer_tab_dummy_label = Label(tab_Digitizer, text="")
digitizer_tab_dummy_label.grid(column=0, row=0)

#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
#Channel settings

frame_channel_registers = Frame(tab_Digitizer)
frame_channel_registers.grid(column=0, row=4)

#Channel settings - general

frame_general_channel_registers = Frame(frame_channel_registers)
frame_general_channel_registers.grid(column=0, row=2, padx=25, sticky=W)

#Channel settings - PSD

frame_PSD_channel_registers = Frame(frame_channel_registers)
frame_PSD_channel_registers.grid(column=0, row=4, padx=25, sticky=W)

#Charge sensitivity
def charge_sensitivity_selection(event):
     print ("~!@#$%^&*()_+")
     #print ("Charge sensitivity       :", var_charge_sensitivity.get() )
     pass

lbl_charge_sensitivity = Label(frame_PSD_channel_registers, text="Charge sensitivity")
lbl_charge_sensitivity.grid(column=0, row=3, padx=25, sticky=W)


var_charge_sensitivity = StringVar(frame_PSD_channel_registers)
charge_sensitivity     = OptionMenu(frame_PSD_channel_registers, var_charge_sensitivity, '', command=charge_sensitivity_selection)
charge_sensitivity.grid(column=1, row=3, sticky=W)


#ADC range
def update_options(*args):
    adc_range=opt_ADC_range[var_ADC_range.get()]
    var_charge_sensitivity.set(adc_range[0])
    
    menu = charge_sensitivity['menu']
    menu.delete(0, 'end')
    
    for adc in adc_range:
         menu.add_command(label=adc, command=lambda sens=adc: var_charge_sensitivity.set(sens) )
         
def ADC_range_selection(event):
    print ("ADC range                 :", var_ADC_range.get() )
    pass
    
lbl_ADC_range = Label(frame_general_channel_registers, text="ADC range")
lbl_ADC_range.grid(column=0, row=1, padx=25, sticky=W)

opt_ADC_range = {'0.5':['1.25', '5', '20', '80', '320', '1280'], '2':['5', '20', '80', '320', '1280', '5120']}
var_ADC_range = StringVar(frame_general_channel_registers)
var_ADC_range.trace('w', update_options)
ADC_range     = OptionMenu(frame_general_channel_registers, var_ADC_range, *opt_ADC_range.keys(), command = ADC_range_selection)
var_ADC_range.set('2')
ADC_range.grid(column=1, row=1, sticky=W)

#This keeps the window open - has to be at the end
window.mainloop()
1

There are 1 best solutions below

2
On

If you comment out the line var_ADC_range.trace('w', update_options), the following happens:

  • the "Charge sensitivity" menu is "empty", meaning it has a single empty entry
  • if you select this entry, the charge_sensitivity_selection function is called
  • -> you see the ~!@#$%^&*()_+ printed

This means that the update_options function overwrites the command that will be called when an entry within the "Charge sensitivity" menu is selected. This happens in the line:

menu.add_command(label=adc, command=lambda sens=adc: var_charge_sensitivity.set(sens) )

One could try to replace the lambda expression in the line above with a function object like this:

...
charge_sensitivity.grid(column=1, row=3, sticky=W)  # <- your code

class run_on_charge_sens_selection(object):         # <- the function object

    def __init__(self, sens):
        self.sens = sens

    def __call__(self):
        # this function is called on menu selection
        var_charge_sensitivity.set(self.sens)
        charge_sensitivity_selection(self.sens)


#ADC range                                          # <- again your code
def update_options(*args):
    adc_range=opt_ADC_range[var_ADC_range.get()]
    var_charge_sensitivity.set(adc_range[0])
      
    menu = charge_sensitivity['menu']
    menu.delete(0, 'end')
      
    for adc in adc_range:                           # and now we use the function object
                                                    # instead of the lambda expression
         menu.add_command(label=adc, command=run_on_charge_sens_selection(adc))
...

I use the function object to pass the sens parameter, there seems to be no event when the function is called.

I'm not that deep into tkinter and I have never used trace, so most probably there are easier solutions.