Tkinter generate and invoke virtual event between different widgets

11,123

Solution 1

You need to add the binding to the widget that gets the event. In your case you are generating the event on the menu, so you need to bind to the menu.

You could also generate the event on the canvas, and keep the binding on the canvas. Or, associate the event with the root window, and bind to the root window.

A common technique -- employed by tkinter itself in some cases -- is to generate the event on the root window, and then have a single binding on the root window (or for all windows with bind_all) for that event. The single binding must then determine which window to affect by some means (often, for example, by getting the window with the keyboard focus).

Of course, if you have a way of determining which widget gets the binding, you can use that method at the time you generate the event to generate the event directly on the appropriate widget.

For more information see Events and Bindings, specifically the section of that document with the heading "Instance and Class Bindings".

Solution 2

Here's my sample code for creating custom virtual events. I created this code to simulate calling servers which take a long time to respond with data:

#Custom Virtual Event

try:
    from Tkinter import *
    import tkMessageBox
except ImportError:
    try:
        from tkinter import *
        from tkinter import messagebox
    except Exception:
        pass

import time
from threading import Thread

VirtualEvents=["<<APP_DATA>>","<<POO_Event>>"]

def TS_decorator(func):
    def stub(*args, **kwargs):
        func(*args, **kwargs)

    def hook(*args,**kwargs):
        Thread(target=stub, args=args).start()

    return hook

class myApp:
    def __init__(self):
        self.root = Tk()
        self.makeWidgets(self.root)
        self.makeVirtualEvents()
        self.state=False
        self.root.mainloop()

    def makeWidgets(self,parent):
        self.lbl=Label(parent)
        self.lbl.pack()
        Button(parent,text="Get Data",command=self.getData).pack()

    def onVirtualEvent(self,event):
        print("Virtual Event Data: {}".format(event.VirtualEventData))
        self.lbl.config(text=event.VirtualEventData)

    def makeVirtualEvents(self):
        for e in VirtualEvents:
            self.root.event_add(e,'None') #Can add a trigger sequence here in place of 'None' if desired
            self.root.bind(e, self.onVirtualEvent,"%d")

    def FireVirtualEvent(self,vEvent,data):
        Event.VirtualEventData=data
        self.root.event_generate(vEvent)


    def getData(self):
        if not self.state:
            VirtualServer(self)
        else:
            pooPooServer(self)

        self.state = not self.state


@TS_decorator
def VirtualServer(m):
    time.sleep(3)
    m.FireVirtualEvent(VirtualEvents[0],"Hello From Virtual Server")

@TS_decorator
def pooPooServer(m):
    time.sleep(3)
    m.FireVirtualEvent(VirtualEvents[1],"Hello From Poo Poo Server")


if __name__=="__main__":
    app=myApp()

In this code sample, I'm creating custom virtual events that are invoked once a simulated server has completed retrieving data. The event handler, onVirtualEvent, is bound to the custom virtual events at the root level.

Simulated servers will run in a separate thread of execution when the push button is clicked. I'm using a custom decorator, TS_decorator, to create the thread of execution that the call to simulated servers will run in.

The really interesting part about my approach is that I can supply data retrieved from the simulated servers to the event handlers by calling the FireVirtualEvent method. Inside this method, I am adding a custom attribute to the Event class which will hold the data to be transmitted. My event handlers will then extract the data from the servers by using this custom attribute.

Although simple in concept, this sample code also alleviates the problem of GUI elements not updating when dealing with code that takes a long time to execute. Since all worker code is executed in a separate thread of execution, the call to the function is returned from very quickly, which allows GUI elements to update. Please note that I am also passing a reference to the myApp class to the simulated servers so that they can call its FireVirtualEvent method when data is available.

Solution 3

Eventually I used Bryan's solution with some improvements (I wanted to keep some separation between modules to develop them parallelly).

General idea:

  • add method to save list of 'listener' widgets for particular virtual event

  • during root/app setup, configure "binding nets" between widgets with custom method;

  • add bind for particular virtual event in 'listener' widgets;
  • when virtual event should be generated, the 'generator' widget fires up event for all registered "listener" widgets with when='tail' parameter to avoid immediately invoking.

configure binding nets:

virt_event = '<<open file menu>>'

class mainApp:
    def __init__(self):
        self.root = tk.Tk()
        self.menu = myMenu(self.root)
        self.canvas1 = myCanvas(self.root)
        self.canvas2 = myCanvas(self.root)
        return

    ''' some init and setup widgets etc. '''

    def root_bindings():
        listeners_list = [self.canvas1, self.canvas2]
        self.menu.add_evt_listeners(virt_event, listeners_list)
        return

bind virtual events in widgets:

class myCanvas(tk.Canvas):
    def __init__(self, parent):
        tk.Canvas.__init__(self, parent)
        self._event_bindigs()
        return

   def _event_bindings(self):
       self.bind(virt_event, self.on_open_file)
       return

   def on_open_file(self, event):
       print('open file event')
       return

add method to 'generator' widget to save list of 'listener' widgets:

class myMenu(tk.Menu):
    def __init__(self, parent):
        tk.Menu.__init__(self, parent)
        self.listeners = {} #dict to keep listeners
        return

    def add_event_listeners(self, event, listeners):
        if not isinstance(listeners, list):
            listeners = [listeners]
        if(event in self.events_listeners.keys()):
            self.events_listeners[event] += listeners
        else:
            self.events_listeners[event] = listeners
        return

    ''' some menu components setup including internal events'''

    def open_file(self, filename):
        ''' some internal handling menu '''
        self.open_file_handler(filename)

        ''' broadcast event to registered widgets '''
        for listener in self.event_listeners[virt_event]:
            listener.event_generate(virt_event, when='tail')
        return
Share:
11,123
voldi
Author by

voldi

Updated on June 15, 2022

Comments

  • voldi
    voldi about 2 years

    during writing some simple gui app in tkinter I met some small problem. Let's say I have custom menu widget (derived from tk.Menu) and custom canvas widget (derived from tk.Canvas).

    I would like to generate event from menu callback function and invoke it in canvas widget. I need to do it that way because it future I would like to add more widgets which should react to clicked position in the menu.

    I tried to do it that way:

    custom menu:

    class MainMenu(tk.Menu):
    
        def __init__(self, parent):
           tk.Menu.__init__(self, parent)
           self.add_comand(label='foo', self._handler)
           return
    
        def _handler(self, *args):
            print('handling menu')       
            self.event_generate('<<abc>>')
            return
    

    custom canvas:

    class CustomCanvas(tk.Canvas): 
        def __init__(self, parent, name=''):
            tk.Canvas.__init__(self, parent)
            self.bind('<<abc>>', self.on_event)
            return
    
        def on_event(self, event):
           print(event)
           return
    

    When I click position in menu, _handler callback is invoked properly and event <> is generated, but on_event callback is no invoked. I've tried to add when='tail' parameter, add self.update() etc. but without any result. Anybody knows how to do it?

  • voldi
    voldi almost 9 years
    Thanks for answer Bryan! Anyway I wanted to use events as a method to set communication between different widgets. For my purpose generating and binding event in the same module is useless. Using root window as bypass is the only working solution but quite messy (needs to pass root window reference to every widget and bind any virtual event in root window module)
  • Russell Smith
    Russell Smith almost 9 years
    @voldi: that's just how events work. Tkinter can't allow every widget to respond to every event or it would be chaos. A binding only fires for specific events in specific widgets. Your other option is to use the technique Tkinter uses for some of it's own widgets -- there is a single binding to "all" , and then then bound function must figure out which window the event applies to (eg: for the <<Copy>> event it figures out which widget has the keyboard focus, and does the copy on that winget). See "Instance and class bindings": effbot.org/tkinterbook/tkinter-events-and-bindings.htm