[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]
[pygame] "Driftwood" UI
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()