Next up is some more recent code for buttons. Still full of obvious flaws, but a big improvement over my last try. First up are some changes to myevents.py:
- ditch Event.name - it was pretty much duplicate information to Event.kind, which was much more useful.
- ditch Listener class - it was mostly a symbol of relationships, and only one class used it without overwriting everything it did.
Screen View
The basic difference is simply that this view is more general now. Instead of hard coding the size and color, they're set in init. It may only be a few lines different - but it's a much better class for it.
class ScreenView: """ Create a window which repaints everything at every tick event. """ def __init__(self, manager, size, bgcolor): """ Create a new window with a specified size and background color """ pygame.init() manager.register(TickEvent, self) self.screen = pygame.display.set_mode( size ) self.bg = bgcolor self.screen.fill(self.bg) def notify(self, event): """ Listens to Tick Events. Repaints the screen. """ pygame.display.flip() self.screen.fill(self.bg)
main.py
The responsibilities of each class, and how they communicate, has changed up quite a bit. Here's what each piece should do:
- track their own status
- track position / size
- respond to mouse actions
- (should not) send out events
- draw something button specific to the screen
- have a default way to draw each button status
- nothing else!
- knows about all the buttons (list buttons)
- associates specific buttons with specific views - note that all the views behave the same in this version - but if loading images or displaying text for the button this would not be the case
- know what happens when each button is pressed
- deal with events
- wrap pygame mouse events
- search through pygame events for mouse events every tick
- button model
- button view
- button mediator (new!)
- mouse event
- mouse interface
I've decided to call the components that react directly to user input as interfaces (hence, mouse interface). I'm pretty sure that's a bad naming convention, since interface has its own implications in programming - but like so many other things, it's going to stay until I come up with something better. Meanwhile, notice that the button mediator (a new component in this version) does most of the work. What I've tried to encapsulate in it is the logic behind the buttons. Back to something resembling the model, view, and logic, with some events and human interfacing to tie it all together.
button model
In some terrible attempt to use a static dictionary to store the possible statuses of a button, I ended up with a garbled mess. You'll see in just a moment. The point is, I ask that you please pass it by without comment (unless you have a suggestion and I haven't already fixed it). I haven't quite decided how to get the effect I want without making it worse and harder to understand. I think I'm being confused by a vague memory of what miniscule amout of perl that I know. Anyway, buttons are delightedly simple now. As suggested above, they only do three things. Storing information is trivial. So naturally, the key is how they react to mouse events. They also have a pair of functions, enable and disable, which modify the status, since there's no logical mouse action to cause that. Notice that the button model is abstracted one level away from events - it neither listens to nor posts them.
class ButtonModel: # work around for now - I like the notation STATUS.normal better NORMAL = 'normal' DOWN = 'down' HOVER = 'hover' DISABLED = 'disabled' STATUS = { 'normal':0 , 'down':1 , 'hover':2 , 'disabled':3 } def __init__(self, rect): self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal self.rect = rect def enable(self): if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled: self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal def disable(self): self.status = ButtonModel.STATUS[ButtonModel.DISABLED] #.disabled
The mouseAction function actually does two things. The first is update the button status. The second is to return a value that indicates when a button is clicked. First, we check if the button is disabled. If it is, there is no need to do anything else. Second, we check if the mouse position collides with the button. If it does not, the button must be in a neutral state (Normal Status). If the mouse is pressed over the button, but released elsewhere (or vise versa), the button should change status but never register a click. The initial reason was mostly visual - most buttons appear to be pressed down when holding the mouse over them. However, it's worth while to store this status in the button model for click checking. If the button has not been pressed down, it can not be clicked. If the mouse is released while over the button, and it was pressed down, it should be. This is the way that this method determines the click.
def mouseAction(self, mouseevent): if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: return #.disabled: return if self.rect.collidepoint( mouseevent.pos ) : if mouseevent.type == MOUSEBUTTONDOWN: self.status = ButtonModel.STATUS[ButtonModel.DOWN] #.down elif mouseevent.type == MOUSEBUTTONUP and self.status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down: self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal return True # BUTTON HAS BEEN CLICKED else: self.status = ButtonModel.STATUS[ButtonModel.HOVER] #.hover else: self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal
button view
It seemed like a good idea for the button view to have a static draw method. So it does. I haven't yet used it. The idea was that if, for some reason, I felt the need to store visual information about the button elsewhere, I could simply blit the button visual onto the screen, using the button view class without needing an instance. I haven't yet thought up a reason to organize the code that way, but the idea is planted and ready for use in case I need it. Anyway, what the view really does is store the screen. To draw to the screen, it requires the position/size of the button, and the status. In this simple case, it uses the status to determine what color to draw in. I initially had the draw method look like:
def draw(self, rect, status): if status == ButtonModel.STATUS[ButtonModel.NORMAL]: #.normal: self.screen.fill( white, rect ) elif status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down: self.screen.fill( blue, rect ) elif status == ButtonModel.STATUS[ButtonModel.HOVER]: #.hover: self.screen.fill( green, rect ) elif status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled: self.screen.fill( grey, rect )But now, just right this very moment, as I was about to copy and paste it here, as I am writing this, it occured to me to do this instead:
color = black if status == ButtonModel.STATUS[ButtonModel.NORMAL]: #.normal: color = white elif status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down: color = blue elif status == ButtonModel.STATUS[ButtonModel.HOVER]: #.hover: color = green elif status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled: color = grey self.screen.fill( color, rect )
button mediator
So this critter does a ton of the work. It coordinates all the pieces of information (buttons, actions, and view, etc) by keeping an array for each and using the index. Probably not the best idea, but it works well enough for now. Perhaps I will later make another tidy intermediate class, and just store and array of those. I admit I'm just thinking out loud on that. I'm also not certain if the button action should be stored as part of the model. It certainly could be. Anyway, that gets a bit off topic. In short, this mediator sends mouse events to all the buttons, to find out if the button has been clicked. If it has, it calls the button action function, with provided parameters (that's the etc). It then draws all the button views on every tick and every mouse event. Probably no need to draw on mouse events - but it definately needs to draw on tick events (otherwise the screen will go black when you stop moving the mouse). It would be wise to implement a 'dirty' flag to redraw only bits of the screen, but I'm not feeling up for the complexity of that just yet. My code is a few versions ahead of this right now, but I'm about at the point where visualization needs to ramp up to game-quality animated stuff. So patience, patience.
class ButtonMediator: def __init__(self, manager): self.buttons = [] self.actions = [] self.views = [] self.actionArgs = [] self.manager = manager self.manager.register(MouseEvent, self) self.manager.register(TickEvent, self) def addButton(self, rect, screen, action, actionargs): self.buttons.append( ButtonModel( rect ) ) self.actions.append( action ) self.views.append( ButtonView(screen) ) self.actionArgs.append( actionargs ) def disableButton(self, i): self.buttons[i].disable() def enableButton(self, i): self.buttons[i].enable() def notify(self, event): for i in range( len(self.buttons) ): b = self.buttons[i] if event.kind == MouseEvent.kind: if b.mouseAction(event.mouse): f = self.actions[i] f( self.actionArgs[i] ) self.views[i].draw(b.rect, b.status)
Glue
I'll skip over mouse events and interfaces. Like normal, they'll be included in the monster spewing post of full code at the end (which is immenent now). To make this all come together, we need to do more than just create the button mediator in the main function. We need to add some buttons to it. The rect and screen parts are easy - we've provided things like that before. The tricky bit is the button actions. I've forgotton if I mentioned where I'm aiming with this (in the short term), but long story short, I only want each button to be used once. So, quite simply, clicking on a button disables it. The button action arguments are the index of the button in question. It may sound a bit confusing, but in the next post or two things will become a bit less unclear. They may still be muddy, I make no promises.
def main(): manager = Mediator() mouse = MouseInterface(manager) screen = ScreenView(manager, (400,400), black) buttons = ButtonMediator(manager) # def addButton(self, rect, screen, action, actionargs): buttons.addButton( pygame.Rect(50,50,10,10), screen.screen, buttons.disableButton, 0 ) buttons.addButton( pygame.Rect(100,50,10,10), screen.screen, buttons.disableButton, 1 ) buttons.addButton( pygame.Rect(150,50,10,10), screen.screen, buttons.disableButton, 2 ) buttons.addButton( pygame.Rect(200,50,10,10), screen.screen, buttons.disableButton, 3 ) controller = TickGeneratorController(manager) controller.run()
As a final note, I'd like to point out that you must control + C this program to quit. I've provided nothing, so clicking on the 'x' in the window doesn't do a thing. That'll be fixed fairly soon (in the next two posts, if not the next one. I can't remember which I fixed this in.)
mycolors.py
black = (0,0,0) white = (255,255,255) red = (255,0,0) blue = (0,0,255) green = (0,255,0) grey = (100,100,100) gray = grey
commoncomponents.py
from myevents import * import pygame class QuitEvent(Event): kind = "quit" def __init__(self, quitter=True): Event.__init__(self) self.quit = quitter self.sync = False class ScreenView: """ Create a window which repaints everything at every tick event. """ def __init__(self, manager, size, bgcolor): """ Create a new window with a specified size and background color """ pygame.init() manager.register(TickEvent, self) self.screen = pygame.display.set_mode( size ) self.bg = bgcolor self.screen.fill(self.bg) def notify(self, event): """ Listens to Tick Events. Repaints the screen. """ pygame.display.flip() self.screen.fill(self.bg) class TickGeneratorController: """ Generates Tick Events at a rate of 30 FPS. """ FPS = 30 def __init__(self, manager): """ Create a source for regular timer events. """ self.manager = manager self.manager.register(QuitEvent, self) self.running = True self.clock = pygame.time.Clock() def run(self): """ Begin generating tick events at a rate of 30 per second. Continue until a Quit Event is heard. """ while self.running: self.manager.post( TickEvent(pygame.time.get_ticks()) ) self.clock.tick(self.FPS) # throttle back the CPU def notify(self, event): """ Listen for quit events. If one is received with a quit = true value, Stop from running. """ if self.running: if event[QuitEvent.kind]: # if it is set to false - keep running self.running = False
myevents.py
from weakref import WeakKeyDictionary 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.sync = True self.generic = None # just in case def __repr__(self): """Custom representation for the event.""" string = self.kind + " Event" # should be doing a string manipulation to capitalize first letter for attr in self.__dict__.keys(): if attr[:2] == '__': string += "\n\tattr %s=" %attr elif attr != "kind": # should be the same as just "else" string += "\n\tattr %s=%s" % (attr, self.__dict__[attr]) return string 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 TickEvent(Event): """ Tick events are intended to be time events. They dictate the pace of the program. """ kind = "tick" def __init__(self, time=None): """Set the name and tick properties.""" Event.__init__(self) self.tick = time class EventManager: """ A manager for coordinating communication. A glorified event listener. """ 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): """Post an event to all the registered listeners""" for listener in self.listeners: listener.notify(event) class Mediator: """A collection of EventManagers, filtered by event type (kind).""" def __init__(self): """Set up a Mediator""" self.managers = {} self.events = [] def register(self, clazz, listener): """Register a listener for a particular class of event""" self.add(clazz) self.managers[clazz.kind].register(listener) def add(self, clazz): """Add a manager for a class of event explicitly.""" if not clazz.kind in self.managers: self.managers[clazz.kind] = EventManager() def drop(self, clazz): """Drop a manager for a class of event.""" self.managers[clazz.kind] = None def registerAll(self, listener): """ Register an event to listen to all current classes of events. Note: does nothing with event classes which have not yet been added as listeners (either indirectly by registering a listener to it, or directly with the add method. """ for kind in self.managers: self.managers[kind].register(listener) def unregister(self, clazz, listener): """Unregister a listener for a particular classes of events.""" if clazz.kind in self.managers: self.managers[clazz.kind].unregister(listener) def unregisterAll(self, listener): """Unregister a listener for all classes of events.""" for kind in self.managers: self.managers[kind].unregister(listener) def post(self, event): """ Post an event to all interested listeners. Note that only special events with a sync value of False are posted immediately to listeners. All other events are posted in bunches, marked by a Tick Event. """ if event.kind == TickEvent.kind: self.events.append(event) temp = self.events self.events = [] for e in temp: if e.kind in self.managers: self.managers[e.kind].post(e) else: if event.sync: self.events.append(event) else: if event.kind in self.managers: self.managers[event.kind].post(event)
main.py
from commoncomponents import * from myevents import * from pygame.locals import * from mycolors import * import pygame class ButtonModel: # work around for now - I like the notation STATUS.normal better NORMAL = 'normal' DOWN = 'down' HOVER = 'hover' DISABLED = 'disabled' STATUS = { 'normal':0 , 'down':1 , 'hover':2 , 'disabled':3 } def __init__(self, rect): self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal self.rect = rect def enable(self): if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled: self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal def disable(self): self.status = ButtonModel.STATUS[ButtonModel.DISABLED] #.disabled def mouseAction(self, mouseevent): if self.status == ButtonModel.STATUS[ButtonModel.DISABLED]: return #.disabled: return if self.rect.collidepoint( mouseevent.pos ) : if mouseevent.type == MOUSEBUTTONDOWN: self.status = ButtonModel.STATUS[ButtonModel.DOWN] #.down elif mouseevent.type == MOUSEBUTTONUP and self.status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down: self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal return True # BUTTON HAS BEEN CLICKED else: self.status = ButtonModel.STATUS[ButtonModel.HOVER] #.hover else: self.status = ButtonModel.STATUS[ButtonModel.NORMAL]#.normal class ButtonView: def drawButton(screen, buttonSurface, position): screen.blit(buttonSurface, position) def __init__(self, screen): self.screen = screen def draw(self, rect, status): #if status == ButtonModel.STATUS[ButtonModel.NORMAL]: #.normal: # self.screen.fill( white, rect ) #elif status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down: # self.screen.fill( blue, rect ) #elif status == ButtonModel.STATUS[ButtonModel.HOVER]: #.hover: # self.screen.fill( green, rect ) #elif status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled: # self.screen.fill( grey, rect ) color = black if status == ButtonModel.STATUS[ButtonModel.NORMAL]: #.normal: color = white elif status == ButtonModel.STATUS[ButtonModel.DOWN]: #.down: color = blue elif status == ButtonModel.STATUS[ButtonModel.HOVER]: #.hover: color = green elif status == ButtonModel.STATUS[ButtonModel.DISABLED]: #.disabled: color = grey self.screen.fill( color, rect ) class ButtonMediator: def __init__(self, manager): self.buttons = [] self.actions = [] self.views = [] self.actionArgs = [] self.manager = manager self.manager.register(MouseEvent, self) self.manager.register(TickEvent, self) def addButton(self, rect, screen, action, actionargs): self.buttons.append( ButtonModel( rect ) ) self.actions.append( action ) self.views.append( ButtonView(screen) ) self.actionArgs.append( actionargs ) def disableButton(self, i): self.buttons[i].disable() def enableButton(self, i): self.buttons[i].enable() def notify(self, event): for i in range( len(self.buttons) ): b = self.buttons[i] if event.kind == MouseEvent.kind: if b.mouseAction(event.mouse): f = self.actions[i] f( self.actionArgs[i] ) self.views[i].draw(b.rect, b.status) # redraw on mouse event only = bad bad flicker issues # (black when mouse not being used) class MouseEvent(Event): kind = "mouse" def __init__(self, mouseevent): Event.__init__(self) self.mouse = mouseevent class MouseInterface: def __init__(self, manager): self.manager = manager self.manager.register( TickEvent, self ) def notify(self, event): event = None for event in pygame.event.get(): if event.type == MOUSEBUTTONUP or event.type == MOUSEBUTTONDOWN or event.type == MOUSEMOTION: self.manager.post( MouseEvent (event) ) def main(): manager = Mediator() mouse = MouseInterface(manager) screen = ScreenView(manager, (400,400), black) buttons = ButtonMediator(manager) # def addButton(self, rect, screen, action, actionargs): buttons.addButton( pygame.Rect(50,50,10,10), screen.screen, buttons.disableButton, 0 ) buttons.addButton( pygame.Rect(100,50,10,10), screen.screen, buttons.disableButton, 1 ) buttons.addButton( pygame.Rect(150,50,10,10), screen.screen, buttons.disableButton, 2 ) buttons.addButton( pygame.Rect(200,50,10,10), screen.screen, buttons.disableButton, 3 ) controller = TickGeneratorController(manager) controller.run() if __name__ == "__main__": main()
Labels: pygame, python, series 1, tutorial
Leave one.