Tkinter generate and invoke virtual event between

2019-04-10 04:21发布

问题:

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?

回答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 http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm, specifically the section of that document with the heading "Instance and Class Bindings".



回答2:

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


回答3:

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.