How to pass values from a linked menu item in python tkinter?

Question:

How to create a variable to hold and pass label of the item selected from a drop down menu? While I’m here, may I ask how to send a second (spare) variable into a function in tkinter at the same time. I’m not using classes or "OptionMenu".
Here’s what I have…

## In a tkinter I have this...

import tkinter as tk
from tkinter import Toplevel, Button, Tk, Menu, Frame

root = Tk()

xr, yr = 200, 170
rg = str(xr) + "x" + str(yr) # concatinating
root.geometry(rg)

selection = tk.StringVar(root)

print(f"Before option menu selected, selection.get() = {selection.get()}")

def confirm_stage(*args):
    global selection
    print(f"selection.get(): {selection.get()}")
    print(f"selection: {selection}") # Gives PY_VAR0, How to 'decode' PY_VAR0 ?
    # print(f"*selection: {*selection}") # gives and error. Cant use '*' in f-strings
    print(f"selection[0]: {selection[0]}") # gives and error: 'StringVar' object is not subscriptable   

menubar = Menu(root)
stage_menu_item = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Stage", menu=stage_menu_item) # Shows the caption
stage_menu_item.add_command(label="1", command = confirm_stage)
stage_menu_item.add_command(label="2", command = confirm_stage)

root.config(menu=menubar)

root.mainloop()

The "confirm_stage" appears to offer no argument to the called function. It surely can’t be that each single item selected has to point to a new separate and unique function specifically written for each item in the menu, although it would work, the code would be unwieldy.

I’ve seen references to StringVar() which I don’t fully understand, and tried to apply it but no joy, and then how would one pass a useful second accompanying variable?

UPDATE 2: – Stripped down code. I need to get the label of the menu item clicked on.

import tkinter as tk
from tkinter import Toplevel, Button, Tk, Menu, Frame

root = Tk()
root.geometry("200x170")


def donation_fn(selection):
    
    a= clicked_r.get()
    print(f"a= {a}")
    print(f"selection= {selection}")

    b= clicked_m.get()
    print(f"b= {b}")
    print(f"selection= {selection}")



menubar = Menu(root)

clicked_r = tk.StringVar(root)
clicked_m = tk.StringVar(menubar)

list=['1 donation', '2 donations', '3 donations']

donation = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Donations", menu=donation) # Shows the caption
for _ in list: donation.add_command(label=_,  command = lambda: donation_fn(clicked_m.get()))
# port_item.add_command(label=list,  command = lambda: None)


root.config(menu=menubar)

root.mainloop()
Asked By: Micklos

||

Answers:

Is this what you want?

import tkinter as tk
from tkinter import Toplevel, Button, Tk, Menu, Frame

root = Tk()

xr, yr = 200, 170
rg = str(xr) + "x" + str(yr) # concatinating
root.geometry(rg)

# selection = tk.StringVar()

# print(f"Before option menu selected, selection.get() = {selection.get()}")

# def confirm_stage(*args):
#     global selection
#     print(f"selection.get(): {selection.get()}")
#     print(f"selection: {selection}") # Gives PY_VAR0, How to 'decode' PY_VAR0 ?
#     # print(f"*selection: {*selection}") # gives and error. Cant use '*' in f-strings
#     print(f"selection[0]: {selection[0]}") # gives and error: 'StringVar' object is not subscriptable

def confirm_stage(var):
    print(var)

menubar = Menu(root)
stage_menu_item = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Stage", menu=stage_menu_item) # Shows the caption
# stage_menu_item.add_command(label="1", command = confirm_stage)
# stage_menu_item.add_command(label="2", command = confirm_stage)
stage_menu_item.add_command(label="1", command = lambda: confirm_stage(1))
stage_menu_item.add_command(label="2", command = lambda: confirm_stage(2))

root.config(menu=menubar)

root.mainloop()
Answered By: Pragmatic_Lee

If you just want to pass the menu text to the callback and don’t want to do it in every call of .add_command(), then you can create a custom class inheriting from tk.Menu and override the class method add_command(). Inside the override add_command(), you can pass the menu text to the given callback when it is being executed.

import tkinter as tk

class MyMenu(tk.Menu):
    def add_command(self, **kwargs):
        callback = kwargs.pop('command', None)
        text = kwargs.get('label')
        # pass the menu text to the given callback
        super().add_command(**kwargs, command=(lambda: callback(text)) if callback else None)

root = tk.Tk()

xr, yr = 200, 170
root.geometry(f"{xr}x{yr}")

def confirm_stage(menu_text):
    print(menu_text)

menubar = tk.Menu(root)

# Use MyMenu instead of tk.Menu
stage_menu_item = MyMenu(menubar, tearoff=0)
menubar.add_cascade(label="Stage", menu=stage_menu_item)
stage_menu_item.add_command(label="1", command=confirm_stage)
stage_menu_item.add_command(label="2", command=confirm_stage)

root.config(menu=menubar)
root.mainloop()
Answered By: acw1668

The above mentioned "lambda" only solution,

...
stage_menu_item.add_command(label="1", command = lambda: confirm_stage(1))
...

seems to work with static parameters, but if you try it with a loop, eg:

...
for _x in range(5):
   stage_menu_item.add_command(label='Menu'+str(_x), command=lambda: confirm_stage(str(_x)))
...

It doesn’t. "_x" is always "4" no matter what menu item you call. Don’t know why.

The only way it worked for me was:

def _set(x:str):
    return lambda: confirm_stage(x)

...

for _x in range(5):
    stage_menu_item.add_command(label=str(_x), command=_set(str(_x)))

Which renders the wanted result of: "0", "1", "2", "3", "4"

If anyone can enlighten me, why the above easy solution (without _set function) is not working, go ahead.

I forgot to mention I replaced the MyMenu with tk.Menu, since I didn’t liked that approach.

Answered By: ARJS Work
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.