Unbound
Unbound

Reputation: 118

Python tkinter: Delete Menu checkbutton

I want to delete a menu checkbutton when i right click on it.
Its usually done with bind("Mouse3", deletefunction) , BUT i need an actual instance of a checkbutton to bind it with, and the only way to add a checkbutton to a menu i know is a add_checkbutton() method (and i have no access to instance this way). Is there any way i could do this?

import tkinter as tk

root = tk.Tk()
menubar = tk.Menu(root)
view_menu = tk.Menu(menubar, tearoff=0)
view_menu.add_checkbutton(label="Right click on me to delete", onvalue=1, offvalue=False)
# I want to do something like this:
# c = Checkbutton(label="Right click on me to delete")
# c.bind("Mouse3", my_delete_function())
# view_menu.add(c)
menubar.add_cascade(label='View', menu=view_menu)
root.config(menu=menubar)
root.mainloop()

Upvotes: 1

Views: 873

Answers (1)

r.ook
r.ook

Reputation: 13858

To the best of my understanding, there's essentially two parts to your question:

  1. Can the menu bar item be assigned for manipulation after?

  2. Can the referenced item be then bound to an event?


The first answer is, sort of. While you can't exactly assign the object, you can reference it by index like this:

view_menu.delete(0)

Since you added the checkbutton first, it'll have an index of 0. You can either keep track of the indices, or refer to the item by its label. See this related answer from Bryan Oakley. e.g.:

view_menu.delete(view_menu.index("Right click on me to delete"))

The .index() method will locate the index by the menu entry's label, which can be handy unless you have the same label more than once...


The second answer, as far as I'm aware, there doesn't seem to be any effective binding for typical events like mouse clicks. However after some search I did come across a rather hidden <<MenuSelect>> binding that at least triggers an event. That by itself is not useful to your quest, but you can combine the event state with the checkbutton's command argument as well as a boolean flag to trigger an event on click:

# Add a BooleanVar for flagging:
delete_checkbutton = tk.BooleanVar()

# Add a binding to your view_menu 
view_menu.bind('<<MenuSelect>>', event_state)

# Define the callback function:
def event_state(e):
    if bool(e.state & 0x0400):  # watch for the Mouse Right click state
        # you might opt to use 0x0004 or 0x0001 instead
        # i.e. Ctrl+click or Shift+Click
        delete_checkbutton.set(True)
    else:  # If the state is not right click, reset the flag
        delete_checkbutton.set(False)

# Define a self_delete command for the checkbutton
def self_delete():
    if delete_checkbutton.get():
        view_menu.delete(view_menu.index("Right click on me to delete"))

# Add the command to your checkbutton
view_menu.add_checkbutton(label="Right click on me to delete", onvalue=lambda:print('hey'), offvalue=False, command=self_delete)

Note: You will actually have to hold right click and then left click on the checkbutton to delete it. Obviously the drawback is you have now triggered the on/off value, and you might need to have some additional handling on those.

If right + left click is too awkward, Ctrl/Shift is another mod state you might consider.

Another side note: I'm a proponent of OOP when it comes to tkinter, it makes accessible variables and flags much easier without needing to worry the global and nonlocal namespaces. Here since delete_checkbutton is set in the global namespace I avoied using the global keyword and accessed it via the tk.BooleanVar() object. However if you were to use a python boolean (e.g. flag = True) then it won't be as effective unless you indicate global flag in both functions. If however you took an OOP approach you can reference the flags directly via self.flag without ambiguity.


Finally, here are the comprehensive changes implemented into your code for sampling:

import tkinter as tk

def event_state(e):
    if bool(e.state & 0x0400):
        # you might opt to use 0x0004 or 0x0001 instead
        # i.e. Ctrl+click or Shift+Click
        delete_checkbutton.set(True)
    else:
        delete_checkbutton.set(False)

def self_delete():
    if delete_checkbutton.get():
        view_menu.delete(view_menu.index("Right click on me to delete"))

root = tk.Tk()
menubar = tk.Menu(root)
delete_checkbutton = tk.BooleanVar()
view_menu = tk.Menu(menubar, tearoff=0)
view_menu.add_command(label='dude', command=lambda: print('dude'))
view_menu.add_checkbutton(label="Right click on me to delete", onvalue=lambda:print('hey'), offvalue=False, command=self_delete)
menubar.add_cascade(label='View', menu=view_menu)
root.config(menu=menubar)
view_menu.bind('<<MenuSelect>>', event_state)
root.mainloop()

All that said, I am of the opinion that this is not a very smooth User Experience and is somewhat confusing. Just the permanent deletion of the menu item alone is questionable at best, combined with the method you are trying to call upon the deletion feels even more contrived. I'd suggest revisiting your UX flow to consider how to streamline this.

Upvotes: 1

Related Questions