0

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()
maij
  • 4,094
  • 2
  • 12
  • 28
Thanos
  • 594
  • 2
  • 7
  • 28
  • why are You using `pass` after `print()` functions? – Matiiss Apr 20 '21 at 18:35
  • @Matiiss Doesn't have any effect. I think that OP just forgot to remove it when they added the `print` statement. I sometimes do that as well :D – TheLizzard Apr 20 '21 at 19:10
  • @Matiiss It's exactly what TheLizzard said. I don't think that's the issue though, is it? I'm not experienced so I might be wrong. – Thanos Apr 20 '21 at 19:37
  • I would not use `OptionMenu` at all. I would use `ttk.Combobox`. – Delrius Euphoria Apr 20 '21 at 23:34
  • Why have you not used `trace` on variable of 2nd optionmenu? – Delrius Euphoria Apr 21 '21 at 00:31
  • Try changing the line `menu.add_command(label=adc, command=lambda sens=adc: var_charge_sensitivity.set(sens))` to `menu.add_command(label=adc, command=tk._setit(var_charge_sensitivity, adc, charge_sensitivity_selection))`. `tk._setit()` is a class used by `OptionMenu` internally to create the menu items. – acw1668 Apr 21 '21 at 08:13
  • @acw1668 That seems to do the trick! Thanks! – Thanos Apr 21 '21 at 17:09

1 Answers1

1

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.

maij
  • 4,094
  • 2
  • 12
  • 28
  • `command=run_on_charge_sens_selection(adc)` will be executed before even choosing it. – Delrius Euphoria Apr 20 '21 at 23:33
  • 1
    @CoolCloud `run_on_charge_sens_selection(adc)` only initializes a function object (or functor) of type `run_on_charge_sens_selection` with the parameter `adc`. It will be executed later when somebody clicks on an entry in the "Charge sensitivity" menu, and then the `__call__` method of the functor will be executed. – maij Apr 21 '21 at 00:23