可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.