I've decided to expand - I'll be making multiple event types. Moreover, there will be multiple Event Managers to prevent objects from being flooded with events they don't care about.
The best way, I concluded, to handle multiple Event Managers was to have a Manager for that. That way objects will still only need to interact with one class, using (virtually) the same interface. Then I won't have to recode much and there will be rainbows and unicorns. Or something. There will certainly be something. *Ah-hem*. I've called the new class "Manager". It's so uncreative and nondescript that I'm sure it'll have to change. Something better than Manager, that isn't "EventManagerManager" either (because reading that a year from now I'll have less than no idea what it means). Things Manager needs to do:
- register listeners
- post events using the same interface (post(self, event))
- containt multiple Event Managers
- post events in the order they are recieved, only when a tick event arrives (maintain current behavior)
Events
First things first, the Event class will be gaining a new feature - type. Since 'type' is a keyword, I'll be using 'kind' instead. This is not kind as in "kind or mean?", but as a synonym for type (like "what type is it?"). Since I plan on subclassing Event, this will be a static property, done like this:
class Event: kind = "generic" def __init__(self): ...In all other ways, Event is the same. But there will be some additional classes needed. I'll start with Tick Event. This type of event will be defined in myevents.py, while the others will be defined in main.py. The reason is that Tick Event will be used (indirectly) in the logic for Manager. This way, the only specific event type is provided in the module with the other event classes. This is not entirely necessary, since the manager won't require the events it recieves to be related to the Event class at all - they only need to act like it. This is why I'm using the property kind, so that I don't have to test if I have an instance of any event class.
Tick Event:
class TickEvent(Event): """ Tick events are intended to be time events. They dictate the pace of the program. """ kind = "tick" def __init__(self, time=None): Event.__init__(self) """Set the name and tick properties.""" self.name = "Tick" self.tick = timeAlthough I haven't shown it yet, there's a reason I named the kind "tick". This is because the only property of a Tick Event that cannot be found in Event is the "tick" property. I will use the same convention for all my new event types.
Event Managers
The Event Manager class is going to be changning a fair bit too. It's actually going to revert and become simpler. A large part of the reason for this is to preserve the order of events. Otherwise, all the events of a particular type (such as Fibonacci events) will be posted at once by Event Manager, followed by all the events of the next type. Much of the logic currently in Event Manager will be transfered to Manager. So, like Event Manager before it, Manager will need a list of events. It will also need some Event Managers. Since I plan on posting to Event Managers based on kind, I chose to use a dictionary, keyed by kind.
I'll just go ahead and show the whole class at once, as well as the modified Event Manager.
class EventManager: """ A manager for coordinating communication between Model, View and Controller """ def __init__(self): """Set up a dictionary to hold references to listeners""" self.listeners = WeakKeyDictionary() def register(self, listener): """Register a listener. It must implement the function notify(event)""" self.listeners[ listener ] = 1 def unregister(self, listener): """Unregister a listener""" if listener in self.listeners: del self.listeners[ listener ] def post(self, event): for listener in self.listeners: listener.notify(event) class Manager: """A collection of EventManagers, filtered by event type (kind).""" def __init__(self): self.managers = {} self.events = [] def register(self, kind, listener): self.add(kind) self.managers[kind].register(listener) def add(self, kind): if not kind in self.managers: self.managers[kind] = EventManager() def drop(self, kind): self.managers[kind] = None def registerAll(self, listener): """"Register for all managers - does nothing if a manager is added afterwards.""" for kind in self.managers: self.managers[kind].register(listener) def unregister(self, kind, listener): if kind in self.managers: self.managers[kind].unregister(listener) def unregisterAll(self, listener): for kind in self.managers: self.managers[kind].unregister(listener) def post(self, event): if event.kind == TickEvent.kind: self.events.append(event) temp = self.events self.events = [] #print 'posting some events' for e in temp: #print e if e.kind in self.managers: self.managers[e.kind].post(e) else: if event.sync: self.events.append(event) else: for m in self.managers: self.managers[m].post(event)
Other
Another small change to the myevents.py file is to Listeners. This is a real quick one line change - in the init function, use registerAll instead of register.
We'll finish up with some changes to the test code at the bottom, then move on to main.py.
e = TickEvent() print e[TickEvent.kind] e = TickEvent('hi') print e[TickEvent.kind] print e['anotherAttribute']I'm going to skip the explaination on this bit, as it isn't really important.
main.py
I'll try to skip going into too much detail (I have more code to write after I finish this post) about little changes like from myevents import * and just post the full code (again). A brief summary of changes: Introduce QuitEvent, FibonacciEvent, minor changes to TickGenerator, and general use of Manager over Event Manager.
New Event Types
The new Events are pretty similar in form to the ones previously defined - they set a kind, name, sync and 'kind' property (such as the tick property for events of kind 'tick'). The quit event is exactly what you'd expect. As before, it is the only event with a sync value of False so that there is no delay in it's application. You can take a peak at those from the full code, as I can see no reason to post excess code chunks.
Changes to Listeners
Our implemented listeners (like Tick Generator Controller) have changed to adapt to the new Manager. The most notable change in Tick Generator is in init and notify. In init, you will see that we now register to a specific kind of event. This controller only listens to Quit Events - other events are of no interest. This means that in notify, we no longer need to check if the event has a 'quit' property - it will. For some other listeners, this will not be possible because they'll need to hear more than one event type.
For the Quit Controller, the listener type must be set, since it can be more than one thing. Once constructed, a particular instance of this controller only listens to one event type, so its notify method is similar to Tick Generator.
The Event Logger still listens to everything - notice that I didn't use __init__. Due to the change I made in Listener, it will try to listen to all events (registerAll). However there is a flaw - there's the possibility that some of the event types will not be in the Manager when this is called. This is why the Manager also has an add method, which I'll use in the main function.
The Fibonacci Model also listens to only one type of event. This one is only interested in Tick Events. So, in short, none of the listeners have to use type checking in their notify functions. The only listener that hears more than one kind of event only prints them out, using methods available to all event types through the parent class.
main function
The main function has changed surprisingly little. After creating manager (an instance of Manager, instead of EventManager), we add the events explicitly. Again, this is so we can be absolutely sure the Event Logger hears all event types. Notice also how similar all the listeners are to their previous constructions - most are exactly the same. The results of the initializations are much different, but the method calls to create our instances are not. Neat.
code (myevents)
from weakref import WeakKeyDictionary class Listener: """ A superclass for listeners. Note that a listener must save the reference to the manager if it needs to send events, as opposed to just recieve them. """ def __init__(self, manager): """Register with the manager automatically.""" manager.registerAll(self) def notify(self, event): """Default implementation for recieving events.""" pass class Event: """ A superclass for events to be handled by EventManager """ kind = "generic" def __init__(self): """Set the name of the Event. Subclasses may have other attributes.""" self.name = "Generic" self.sync = True def __repr__(self): """Custom representation for the event.""" return self.name + " Event" def attributes(self): """Return a formated string of the attributes""" result = '' for attr in self.__dict__.keys(): if attr[:2] == '__': result = result + "\tattr %s=\n" % attr else: result = result + "\tattr %s=%s\n" % (attr, self.__dict__[attr]) return result def hasAttribute(self, attr): """Check if the event has a given attribute""" return attr in self.__dict__.keys() def __getitem__(self, item): """Get the value of an attribute""" if item in self.__dict__.keys(): return self.__dict__[item] return None class EventManager: """ A manager for coordinating communication between Model, View and Controller """ def __init__(self): """Set up a dictionary to hold references to listeners""" self.listeners = WeakKeyDictionary() def register(self, listener): """Register a listener. It must implement the function notify(event)""" self.listeners[ listener ] = 1 def unregister(self, listener): """Unregister a listener""" if listener in self.listeners: del self.listeners[ listener ] def post(self, event): for listener in self.listeners: listener.notify(event) class Manager: """A collection of EventManagers, filtered by event type (kind).""" def __init__(self): self.managers = {} self.events = [] def register(self, kind, listener): self.add(kind) self.managers[kind].register(listener) def add(self, kind): if not kind in self.managers: self.managers[kind] = EventManager() def drop(self, kind): self.managers[kind] = None def registerAll(self, listener): """"Register for all managers - does nothing if a manager is added afterwards.""" for kind in self.managers: self.managers[kind].register(listener) def unregister(self, kind, listener): if kind in self.managers: self.managers[kind].unregister(listener) def unregisterAll(self, listener): for kind in self.managers: self.managers[kind].unregister(listener) def post(self, event): if event.kind == TickEvent.kind: self.events.append(event) temp = self.events self.events = [] #print 'posting some events' for e in temp: #print e if e.kind in self.managers: self.managers[e.kind].post(e) else: if event.sync: self.events.append(event) else: for m in self.managers: self.managers[m].post(event) class TickEvent(Event): """ Tick events are intended to be time events. They dictate the pace of the program. """ kind = "tick" def __init__(self, time=None): Event.__init__(self) """Set the name and tick properties.""" self.name = "Tick" self.tick = time """Test the contents of this file.""" if __name__ == "__main__": e = Event() print e #print e.tick # still errors em = EventManager() class Test(Listener): """Uses the default init.""" def notify(self, event): print "I have recieved a "+ str(event) test = Test(em) em.post(e) em.unregister(test) em.post(e) e = TickEvent() print e[TickEvent.kind] e = TickEvent('hi') print e[TickEvent.kind] print e['anotherAttribute']
code (main)
import pygame from myevents import * """ Some of these will send out more events than are needed - and they are all of type Event. There is probably some use in having separate listeners cateorgies. I'll try to explore that later. It will also log a bunch of "Generic Events" since I haven't put forth any effort to use event types in a better way. """ class QuitEvent(Event): kind = "quit" def __init__(self, quitter=True): Event.__init__(self) self.name = "Quit" self.quit = quitter self.sync = False class FibonacciEvent(Event): kind = "fibonacci" def __init__(self, number=None): Event.__init__(self) self.name = "Fibonacci" self.fibonacci = number class TickGeneratorController(Listener): """ Uses a clock to generate a 30 FPS tick. Currently 30 is a magic number. Note that this class is a Listener, but there was really no need to extend the superclass. It is done here for consistency only. """ FPS = 30 def __init__(self, manager): self.manager = manager # we will be posting events self.manager.register(QuitEvent.kind, self) self.running = True self.clock = pygame.time.Clock() def run(self): while self.running: self.manager.post( TickEvent(pygame.time.get_ticks()) ) self.clock.tick(self.FPS) # throttle back the CPU def notify(self, event): if event[QuitEvent.kind]: # if it is set to false - keep running self.running = False class QuitController(Listener): def __init__(self, manager, goalAttribute, goalValue): self.manager = manager self.manager.register(goalAttribute, self) self.goalAttribute = goalAttribute self.goalValue = goalValue def notify(self, event): #if event.hasAttribute(self.goalAttribute): if event[self.goalAttribute] >= self.goalValue: event = QuitEvent() self.manager.post(event) class EventLoggerView(Listener): """ Logs events to the screen. Note that it does not define init - it uses the default Listener. """ def notify(self, event): print event print event.attributes() class FibonacciModel(Listener): """ Stores some simple information and changes itself on updates. This class will generate events so that we can log them. """ def __init__(self, manager): manager.register(TickEvent.kind, self) self.manager = manager self.last = 0 self.current = 1 manager.post( FibonacciEvent(self.current) ) def notify(self, event): self.last, self.current = self.current, self.last+self.current self.manager.post( FibonacciEvent(self.current) ) def main(): manager = Manager() manager.add(TickEvent.kind) manager.add(FibonacciEvent.kind) manager.add(QuitEvent.kind) model = FibonacciModel(manager) view = EventLoggerView(manager) controller = TickGeneratorController(manager) stop1 = QuitController(manager, TickEvent.kind , TickGeneratorController.FPS*50) # 30 is FPS stop2 = QuitController(manager, FibonacciEvent.kind, 1000) controller.run() if __name__ == "__main__": main()
Labels: MVC, pygame, python, series 1, tutorial
Leave one.