Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Tkinter generate and invoke virtual event between different widgets

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?

like image 623
voldi Avatar asked Aug 04 '15 00:08

voldi


2 Answers

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".

like image 130
Bryan Oakley Avatar answered Nov 15 '22 09:11

Bryan Oakley


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.

like image 33
John Moore Avatar answered Nov 15 '22 09:11

John Moore