I just got this: ---------- Forwarded message ---------- From: Kris Schnee <[EMAIL PROTECTED]> Date: Jan 22, 2008 11:16 AM Subject: [pygame] "Driftwood" UI To: pygame-users@seul.org
Over lunch I cleaned up the latest version of my UI code, which is attached. This version just contains a generic text-displaying widget and a Button that responds to clicking, with a simple solid-color or gradient look. I hope to add more widgets to this by cleaning up and modifying older code. Even in this incomplete version, it might serve as a low-end system for people looking to make a couple of buttons and text labels as quickly and simply as possible. It's public domain if you'd like to use it. Kris #!/usr/bin/python """ Driftwood v3 by Kris Schnee. A simple graphical interface (ie. system of buttons, meters &c). The goal of this system is to provide an interface in a way that is very simple to use, requires no dependencies but Pygame, and doesn't take over your program's event-handling loop. It should serve as a basic UI system for those whose main concern is to get basic interface elements working quickly, as opposed to a full-featured game engine. The interface is divided into "widgets" that can respond to events. To use this module, create a list of widgets, eg "self.widgets = []". Each widget should be passed an "owner" reference. Display the widgets by calling eg.: for widget in self.widgets: widget.Draw() ## Draws to a Pygame Surface object called "screen". To make them respond to events, call in your Pygame event loop: for event in pygame.event.get(): handled = False for widget in self.widgets: handled = widget.HandleEvent(event) if handled: break if not handled: ## Put your own event-handling code here To get information back from the widgets handling events, your game should have a "Notify" function that puts this info on a stack of some kind for your game to react to it. The widgets will call Notify and pass along a dictionary object describing what happened; you can then respond to these events or not. See the demo for how all this comes together. See also my "Ringtale" module for one method of organizing a game's states. Currently available widget types: -Widget (generic) -Button I'm in the process of rebuilding this module from a past version. Module Contents (search for chapter symbols): ~Header~ ~Imported Modules~ ~Constants~ ~Classes~ ~Functions~ ~Autorun~ (the code that runs when this program is run) """ ##___________________________________ ## ~Header~ ##___________________________________ __author__ = "Kris Schnee" __version__ = "2008.1.22" __license__ = "Public Domain" ##___________________________________ ## ~Imported Modules~ ##___________________________________ ## Standard modules. import os ## Third-party modules. import pygame ## Freeware SDL game toolkit from <pygame.org> from pygame.locals import * ## Useful constants ##___________________________________ ## ~Constants~ ##___________________________________ ## Look in these subdirectories for graphics. ## I assume a "graphics" subdir containing subdirs for "interface" and "fonts". GRAPHICS_DIRECTORY = "graphics" INTERFACE_GRAPHICS_DIRECTORY = os.path.join(GRAPHICS_DIRECTORY,"interface") FONT_DIRECTORY = os.path.join(GRAPHICS_DIRECTORY,"fonts") DEFAULT_FONT_NAME = None ##___________________________________ ## ~Classes~ ##___________________________________ class Pen: """A wrapper for pygame's Font class. It offers a simple text-writing function and is used by widgets.""" def __init__(self,filename=DEFAULT_FONT_NAME,size=30,color=(255,255,255)): if filename: filename = os.path.join(FONT_DIRECTORY,filename) self.font = pygame.font.Font(filename,size) self.color = color def Write(self,text,color=None): """Return a surface containing rendered text.""" if not color: color = self.color return self.font.render(text,1,self.color) class Widget: def __init__(self,**options): self.owner = options.get("owner") self.name = options.get("name") self.coords = options.get("coords",(0,0,100,100)) ## Pygame Rect object, or tuple. self.coords = pygame.rect.Rect(self.coords) ## Display surface. self.surface = pygame.surface.Surface(self.coords.size) self.dirty = True ## Graphics options. self.visible = options.get("visible",True) self.pen = options.get("pen",DEFAULT_PEN) self.text = options.get("text","") ## Displayed centered. self.rendered_text = None ## A display surface self.SetText(self.text) """The first color is always used. If a second is given, the window shades downward to the second color.""" self.background_color = options.get("background_color",(0,192,192)) self.background_gradient = options.get("background_gradient",(0,64,64)) self.background_image = options.get("background_image") self.border_color = options.get("border_color",(255,255,255)) ## A hidden surface where I draw my colored background and border for speed. self.background_surface = pygame.surface.Surface((self.coords.w, self.coords.h)).convert_alpha() self.background_surface.fill((0,0,0,0)) """Alpha: Visibility. Alpha can be handled differently for different widgets. For instance you might want a translucent window containing fully opaque text and graphics.""" self.alpha = options.get("alpha",255) ## 0 = Invisible, 255 = Opaque self.BuildBackground() def SetAlpha(self,alpha): self.alpha = alpha def SetVisible(self,visible=True): """Make me drawn or not, independently of my alpha setting.""" self.visible = visible def DrawBackground(self): """Quickly copy my blank background (w/border) to the screen. It's actually drawn using BuildBackround, which must be explicitly called to rebuild it (eg. if you want to change the border style).""" self.surface.blit(self.background_surface,(0,0)) def SetBackgroundImage(self,new_image): self.background_image = new_image self.BuildBackground() def BuildBackground(self): """Redraw the colored background and border (if any). This function is relatively time-consuming, so it's generally called only once, then DrawBackground is used each frame to blit the result.""" self.background_surface.fill((0,0,0,self.alpha)) if self.background_gradient: x1 = 0 x2 = self.background_surface.get_rect().right-1 a, b = self.background_color, self.background_gradient y1 = 0 y2 = self.background_surface.get_rect().bottom-1 h = y2-y1 rate = (float((b[0]-a[0])/h), (float(b[1]-a[1])/h), (float(b[2]-a[2])/h) ) for line in range(y1,y2): color = (min(max(a[0]+(rate[0]*line),0),255), min(max(a[1]+(rate[1]*line),0),255), min(max(a[2]+(rate[2]*line),0),255), self.alpha ) pygame.draw.line(self.background_surface,color,(x1,line),(x2,line)) else: ## Solid color background. self.background_surface.fill(self.background_color) if self.background_image: self.background_surface.blit(self.background_image,(0,0)) pygame.draw.rect(self.background_surface,self.border_color,(0,0,self.coords.w,self.coords.h),1) def DrawAlignedText(self,text,alignment="center"): """Draw text at a certain alignment.""" if alignment == "center": text_center_x = text.get_width()/2 text_center_y = text.get_height()/2 align_by = ((self.coords.w/2)-text_center_x, (self.coords.h/2)-text_center_y) self.surface.blit( text, align_by ) def SetText(self,text): self.text = str(text) self.rendered_text = self.pen.Write(self.text) def Redraw(self): self.surface.fill((0,0,0,self.alpha)) if self.visible: self.DrawBackground() self.DrawAlignedText(self.rendered_text) def Draw(self): if self.dirty: self.Redraw() screen.blit(self.surface,self.coords.topleft) def HandleEvent(self,event): return False ## Never handled by this class. class Button( Widget ): """A widget that sends messages to its owner when clicked.""" def __init__(self,**options): Widget.__init__(self,**options) def HandleEvent(self,event): """React to clicks within my borders. Note that the handling order is determined by the order of widgets in the widget list. Hence you should describe your interface from back-to-front if there are to be widgets "in front" of others.""" if event.type == MOUSEBUTTONDOWN: if self.coords.collidepoint(event.pos): self.owner.Notify({"text":"Clicked","sender":self.name}) return True return False class Demo: """Demonstrates this widget system. There's a stack of events that are generated by the widgets. It would also be possible to use user-defined Pygame events and put those directly onto the Pygame event stack, but the way I'm using allows for more flexibility, because events can be passed from one widget to another, as within a complex widget containing other widgets.""" def __init__(self): self.messages = [] self.widgets = [] self.widgets.append(Widget()) ## Simplest possible widget. self.widgets.append(Button(owner=self,coords=(25,50,200,100),name="bMover",text="Click Me")) self.widgets.append(Widget(owner=self,coords=(50,300,500,50),name="lLabel",text="Jackdaws love my big sphinx of quartz.")) self.widgets.append(Button(owner=self,coords=(50,350,500,50),name="bChange",text="Click here to change above text.",background_gradient=None)) def Notify(self,message): self.messages.append(message) def Go(self): while True: ## Drawing screen.fill((0,0,0)) for widget in self.widgets: widget.Draw() pygame.display.update() ## Logic: Hardware events. for event in pygame.event.get(): ## First, give the widgets a chance to react. handled = False for widget in self.widgets: handled = widget.HandleEvent(event) if handled: break ## If they don't, handle the event here. if not handled: if event.type == KEYDOWN and event.key == K_ESCAPE: return ## Logic: Gameplay events. for message in self.messages: text = message.get("text") if text == "Clicked": sender = message.get("sender") if sender == "bMover": ## Move the button when it's clicked. self.widgets[1].coords[0] += 50 elif sender == "bChange": """Change text in lLabel. Note: I could've made a dict. of widgets instead of a label, so that I could refer to it by name.""" self.widgets[2].SetText("The quick brown fox jumped over the lazy dog.") self.messages = [] ##___________________________________ ## ~Functions~ ##___________________________________ ##__________________________________ ## ~Autorun~ ##__________________________________ pygame.init() DEFAULT_PEN = Pen() screen = pygame.display.set_mode((800,600)) ## Always create a "screen." if __name__ == "__main__": ## Run a demo. pygame.event.set_allowed((KEYDOWN,KEYUP,MOUSEBUTTONDOWN,MOUSEBUTTONUP)) demo = Demo() demo.Go()