Alan Moore
Alan Moore

Reputation: 319

Tkinter: using custom events with a menu on Windows

Using Python 3.9.4

In the interest of loose coupling, I'm trying to implement a main menu in Tkinter that generates custom events rather than directly calling a callback function. The example script shows the basic approach:

import tkinter as tk


root = tk.Tk()
root.geometry('300x200')
label = tk.Label(text='Test')
label.grid()
menu = tk.Menu(root)

root.configure(menu=menu)

submenu = tk.Menu(menu)
menu.add_cascade(menu=submenu, label='Change Text')
submenu.add_command(
    label='Foo',
    command=lambda: menu.event_generate('<<Foo>>')
)
submenu.add_command(
    label='Bar',
    command=lambda: menu.event_generate('<<Bar>>')
)

def setfoo(*_):
    label.configure(text='Foo')

def setbar(*_):
    label.configure(text='Bar')

menu.bind('<<Foo>>', setfoo)
menu.bind('<<Bar>>', setbar)

root.mainloop()

This approach works in Linux, but on Windows the bindings do not work. They appear to be bound to the event, but nothing happens when the menu item is selected.

I am assuming this has to do with the difference between menu implementations on Windows and Linux, but is there a correct way to do this, or a workaround?

EDIT: To help everyone understand better why I'm wanting to do certain things, here is an object-oriented version that more closely aligns with how I'm using Tkinter:

import tkinter as tk

class MainMenu(tk.Menu):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        submenu = tk.Menu(self)
        submenu.add_command(
            label="foo",
            command=lambda: self.event_generate('<<Foo>>')
        )
        submenu.add_command(
            label="bar",
            command=lambda: self.event_generate('<<Bar>>')
        )
        self.add_cascade(menu=submenu, label='Test')


class Application(tk.Tk):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        menu = MainMenu(self)
        self.configure(menu=menu)

        menu.bind('<<Foo>>', print)
        menu.bind('<<Bar>>', print)

if __name__ == '__main__':
    app = Application()
    app.mainloop()

Note that the MainMenu class would be defined in a separate file, so the app global is not available to it. I could use MainMenu.master to get the root window in this case, but that breaks loose coupling as it assumes the root window is the parent of this class. That assumption could break (say the mainwindow gets put in a hamburger menu, or added to another TopLevel that isn't the main app).

Upvotes: 1

Views: 1129

Answers (2)

Bryan Oakley
Bryan Oakley

Reputation: 385970

Menus are are managed by the OS on OSX and Windows, and tkinter has very little control over their behavior. I doubt sending events to the menu on either of those platforms will work.

I would say that sending events to the menu is an anti-pattern. Unless the events are directly related to the menu, you should be sending the events to either the top-level window associated with the menu, the root window, or the window that has the keyboard focus, depending on what the event represents.

Since you're passing in the parent widget as a positional argument, you can use that to get the toplevel window associated with the menu.

For example:

class MainMenu(tk.Menu):

    def __init__(self, parent, *args, **kwargs):
        super().__init__(parent, *args, **kwargs)

        top = parent.winfo_toplevel()
        submenu.add_command(
            label="foo",
            command=lambda: top.event_generate('<<Foo>>')
        )
        ...

Upvotes: 2

TheLizzard
TheLizzard

Reputation: 7680

I think I figured it out, not 100% sure though.

Look at this code:

import tkinter as tk

def trying_to_reach(event):
    print("!")

root = tk.Tk()

text = tk.Text(root)
text.pack()

text.bind("<<Foo>>", trying_to_reach)
root.after(100, text.event_generate, "<<Foo>>")

root.mainloop()

100 ms after the code is run, ! appears on the screen but I comment the text.pack(), nothing is displayed on the screen. Also if I change root.after(100, text.event_generate, "<<Foo>>") to text.event_generate("<<Foo>>"), nothing is displayed on the screen. This proves @BryanOakley's theory that it has something to do with widget's visibility. That means that the problem most likely lies somewhere in tcl/ the functions tcl calls.

The solution would be to have a dummy widget or as @Matiiss suggested just use root.event_generate and root.bind.

Upvotes: 0

Related Questions