TextWidget – A simple text class for PyGame


Introduction

All right, this is just a little tutorial about working with text in pygame. Now, this isn’t the only way to work with text, there are many other methods to do this, in fact much of the time you’ll probably end up using images for interactive text. So this is mainly meant to serve as a informative guide to using the text features in PyGame, and how you might want to implement them.

The TextWidget object discussed in this tutorial can be used to make something that looks like this (but you’d probably want to use better looking colours):


The full source and necessary files for this tutorial can be downloaded here.

So, in order to make this easy to use and very reusable we’re going to create a class called TextWidget in a file called TextWidget.py. The top of the file is full of the standard python initialization:

#! /usr/bin/env python

import pygame

TEXT_WIDGET_CLICK = pygame.locals.USEREVENT + 1

We import pygame and then we set a define TEXT_WIDGET_CLICK , which will be used later on as the event type when the TextWidget is clicked on.

The next thing to do is define the actual class:

class TextWidget(object):

Now the astute among you will recognize that this is a “new-style” python class (i.e. it’s base class is the object class) rather then a classic class. I did this for two reasons:

  1. I wanted to be able to control what happens when people set values in the class. So, for example, if you set the size of the font to be something, I wanted the display to automatically adjust to reflect the new size.
  2. I haven’t really used new-style classes before so I thought I’d try them out.

Note: If you are unfamiliar with properties or new-style classes you might want to give this a read:

As a result of number one above I’m going to use properties for those values that I want to perform some “automatic processing” when their values changes.

The properties that I am going to define are as follows:

  • text – The string of text that the TextWidget will display.
  • colour – The colour that the text will be rendered in.
  • size – The size of the font.
  • font_filename – The filename of the font to use for the text (probably something returned by pygame.font.get_fonts or pygame.font.match_font.
  • highlight – A boolean, whether the font is highlighted or not. (i.e. whether the mouse is over the TextWidget.
  • highlight_cursor – A boolean that controls whether or not to show a hand cursor when the TextWidget is in highlight mode. If the TextWidget is not supposed to be interactive you should set this to False.

There are also a few other important members of the class, but since they are not python properties I have listed them separately:

  • __hand – A hand cursor, which can optionally by displayed when the cursor hovers over the text.
  • dirty – A boolean, that lets the TextWidget know whether or not it needs to draw. If dirty = False it will not draw, if dirty = True it will draw.
  • bold_rect – pygame.Rect – A rectangle that is equal to the size of the highlighted text. It is used so that when we move from highlighted to non-highlighted text all of the text is erased. Otherwise if we just drew updated the normal rectangle, all of the highlighted text that was outside of it would not be redrawn.
  • rect – pygame.Rect – The Rectangle where the text will be drawn. Use this to position your text, but its height and width will always be controlled by the text and the font.
  • highlight_increase – Boolean whether or not the text should increase in size when the cursor is over it. If the text is “non-interactive” then this should be False.
  • tracking – Boolean whether or not the TextWidget is tracking a mouse click. Used so that we can tell whether or not a true “click” has occurred. A true click is one where both the mouse-down and mouse-up events occur on the TextWidget.

To hopefully make the functionality of the TextWidget a bit clearer here is some non-highlighted text and then some highlighted text:

Python pygame text

Python pygame text

The Code

Now we get to the heart of the actual python\PyGame code, where we actually create a working TextWidget object.

So we’ll start off at the top of the file and then work out way downwards in a linear manner.

class TextWidget(object):
    """This is a helper class for handling text in PyGame.  It performs 
    some basic highlighting and tells you when the text has been clicked.
    This is just one of the many ways to handle your text.
    This is a new-style class and I am somewhat new to them so hopefully it 
    all works.
    """
    #Hand Cursor
    __hand_cursor_string = (
    "     XX         ",
    "    X..X        ",
    "    X..X        ",
    "    X..X        ",
    "    X..XXXXX    ",
    "    X..X..X.XX  ",
    " XX X..X..X.X.X ",
    "X..XX.........X ",
    "X...X.........X ",
    " X.....X.X.X..X ",
    "  X....X.X.X..X ",
    "  X....X.X.X.X  ",
    "   X...X.X.X.X  ",
    "    X.......X   ",
    "     X....X.X   ",
    "     XXXXX XX   ")
    __hcurs, __hmask = pygame.cursors.compile(__hand_cursor_string, ".", "X")
    __hand = ((16, 16), (5, 1), __hcurs, __hmask)

The above code is where we create the hand cursor that we will use if the person turns on the highlight_cursor. We first define what the cursor will look like as explained in the PyGame cursor documentation.

The next step is to create all of our properties, now this code may seem a bit complicated, especially since it deals with functions that we have not created yet, but as I explain it, it could become clearer and clearer.

#Text
def __get_text(self):
    return self.__m_text
def __set_text(self, text):
    if (self.__m_text != text):
        self.__m_text = text
        self.update_surface()
text = property(__get_text, __set_text)

The above code creates the TextWidget.text property. The __get_text() function is used to get the text property which is represented by the __m_text variable, and the __set_text() function is the function that is called when you want to set the value of the text property.

What you will notice about the __set_text() function is that it first tests to see if the text that we are setting (text) is any different then the current text (__m_text) if it isn’t any different we don’t do anything.

However if it is different we set __m_text and we call update_surface(). update_surface() is a member function that I will explain in more detail later, but for now all you need to know is that it updates what the widget will display. So since we changed the text we want the widget to update and display the new text.

You can probably see why I wanted to use new-style classes and properties now.

#Colour
def __get_colour(self):
    return self.__m_colour
def __set_colour(self, colour):
    if (self.__m_colour != colour):
        self.__m_colour = colour
        self.update_surface()
colour = property(__get_colour, __set_colour)

Next is the colour property, as you can see it’s basically identical to the text property, so I won’t bother explaining it.

#Size
def __get_size(self):
    return self.__m_size
def __set_size(self, size):
    if (self.__m_size != size):
        self.__m_size = size
        self.create_font()
size = property(__get_size, __set_size)

The next property is the size property, which controls the size of the font. Now this is slightly different in that it calls the create_font() function rather then the update_surface() function that the text and the colour property did.

The reason for this is that the size property is used in the creation of the actual font object that is used to update the surface which is eventually drawn to the screen. As with the update_surface() function I will explain the create_font() function later. Properties that call create_font() do not have to call update_surface() because create_font() calls update_surface().

#Font Filename
def __get_font_filename(self):
    return self.__m_font_filename
def __set_font_filename(self, font_filename):
    if (self.__m_font_filename != font_filename):
        self.__m_font_filename = font_filename
        self.create_font()
font_filename = property(__get_font_filename, __set_font_filename)

Then we have the font_filename property that represents the full path to the font file. So if that changes we need to recreate the font in order to display the text using the correct font.

#Highlight
def __get_highlight(self):
    return self.__m_highlight
def __set_highlight(self, highlight):
    if (not(self.__m_highlight == highlight)):
        #Save the bold_rect
        if (self.__m_highlight):
            self.bold_rect = self.rect
        self.__m_highlight = highlight
        #update the cursor
        self.update_cursor()
        if (highlight):
            self.size += self.highlight_increase
        else:
            self.size -= self.highlight_increase
        if (self.highlight_increase == 0):
            self.create_font()
highlight = property(__get_highlight, __set_highlight)

Next we have the highlight property, the highlight property is a boolean property that controls whether or not the TextWidget should display “highlighted” or not. When a TextWidget is highlighted the font will be displayed in bold, have its size increased by a specific amount, and (if enabled) the hand cursor will be displayed.

So now since the __get_highlight() function is the same as all of the others, I’ll just explain the __set_highlight() function.

First we check to see that the new value is different then the old value. We do this so that we don’t waste time with useless font creations or extra draws.

Then we check to see if the current state of the TextWidget is highlighted, because if it is, we know that it is about to go back to normal (because of the first if.) If that is the case we save the current rect as bold_rect so that the next time we draw we will want bold_rect to be drawn so that all of the previous highlighted text is overwritten.

Then we set the internal highlight value and call the update_cursor() function which will decide whether or not the cursor should be a hand or the default arrow.

Then we set the size of the font depending on whether or not the highlight state has been turned on. If it is we increase the size, and if it is not we decrease the size back down to normal.

Since increasing or decreasing the size will call create_font() we only need to call create_font() if self.highlight_increase is equal to zero, since in that case the value of the size property will not change and the font will not be created.

#Show Highlight Cursor
def __get_highlight_cursor(self):
    return self.__m_highlight_cursor
def __set_highlight_cursor(self, highlight_cursor):
    if (self.__m_highlight_cursor != highlight_cursor):
        self.__m_highlight_cursor = highlight_cursor
        self.update_cursor()
highlight_cursor = property(__get_highlight_cursor, __set_highlight_cursor)

Lastly we have the highlight_cursor property, which is a boolean property that controls whether or not the highlight (hand) cursor should be displayed. If you have made it this far and have understood the rest of the properties then understanding this property shouldn’t be too difficult. Basically it simply checks to see if we should update the property, and if so it updates it and then calls update_cursor() just in case we need the cursor to update right now.

def __init__(self, text="", colour=(0,0,0), size=32
             , highlight_increase = 20, font_filename=None
             , show_highlight_cursor = True):
    """Initialize the TextWidget
    @param text = "" - string - The text for the text widget
    @param colour = (0,0,0) - The colour of the text
    @param size = 32 - number - The size of the text
    @param highlight_increate - number - How large do we want the
    text to grow when it is highlighted?
    @param font_filename = None - string the patht to the font file
    to use, None to use the default pygame font.
    @param show_highlight_cursor = True - boolean - Whether or not to change
    the cursor when the text is highlighted.  The cursor will turn into
    a hand if this is true.
    """
    
    #inits
    self.dirty = False
    self.bold_rect = None
    self.highlight_increase = highlight_increase
    self.tracking = False
    self.rect = None
    
    #property inits
    self.__m_text = None
    self.__m_colour = None
    self.__m_size = None
    self.__m_font_filename = None
    self.__m_highlight = False
    self.__m_font = None
    self.__m_highlight_cursor = False
    self.__m_rect = None
    
    self.text = text
    self.colour = colour
    self.size = size
    self.font_filename = font_filename
    self.highlight = False
    self.highlight_cursor = show_highlight_cursor
    
    self.create_font()

Now we actually start using the class the first thing we do is code the __init__() function in which we initialize all of our data members and properties, and then create the font at the end. This may result in the font being create twice sometimes but it will only happen once when the Widget is created, and it makes sure that the font is always created.

def update_cursor(self):
    if (self.highlight_cursor):
        if (self.highlight):
            pygame.mouse.set_cursor(*self.__hand)
        else:
            pygame.mouse.set_cursor(*pygame.cursors.arrow)

Here is the update_cursor() function, it’s pretty straight forward. First we check whether or not we are supposed to change the cursor at all. If we are then we check to see if we are highlighted or not. If we are highlighted then we use the hand cursor that we created above, and if we are not highlighted we use the built-in pygame arrow cursor.

Drawing and Fonts

Now we finally get to drawing and the font creation that we talked about earlier. The first thing to do is to create our font:

def create_font(self):
    """Create the internal font, using the current settings
    """
    if (self.size):
        self.__m_font = pygame.font.Font(self.font_filename
                                 , self.size)
        self.update_surface()

As you can see this function really is pretty simple, all we need to make sure that we have is a size for the font. Then we create the font using pygame.font.Font() We don’t care if self.font_filename is None since if it is None, then the default font will be loaded.

After the font has been created we call update_surface():

def update_surface(self):
    """Update the current surface, basically render the
    text using the current settings.
    """
    if (self.__m_font):
        self.__m_font.set_bold(self.highlight)
        self.image = self.__m_font.render(self.text
                                        , True
                                        , self.colour)
        self.dirty = True
        if (self.rect):
            # Used the current rects center point
            self.rect = self.image.get_rect(center=self.rect.center)
        else:
            self.rect = self.image.get_rect()

In update_surfacte() we first make sure that our font (__m_font) exists, since if it does not then there is no point in creating any surface. Then we set the font’s bold setting Font.set_bold() function and controlled by the highlight property.

Then we render the font onto a pygame.surface using the Font.render() function and our current text and colour. We also make sure that the font is anti-aliased by passing True as the second parameter.

Then we set self.Dirty to True to make sure that the next time through the draw loop the new surface gets displayed.

The last thing we do in the function is move our text into the correct location. If self.rect has been specified then we move the image into that position and set our rect, otherwise we initialize self.rect to be be surfaces rectangle.

self.rect must eventually be move into some position because Surface.get_rect() always returns a rectangle that starts at 0,0.

def draw(self, screen):
    """Draw yourself text widget
    @param screen - pygame.Surface - The surface that we will draw to
    @returns - pygame.rect - If drawing has occurred this is the 
    rect that we drew to.  None if no drawing has occurred."""
    
    rect_return = None
    if ((self.image)  and  (self.rect) and (self.dirty)):
        if (self.bold_rect):
            """We may need to overwrite the bold text size
            This gets rid of leftover text when moving from 
            bold text to non-bold text.
            """
            rect_return = pygame.Rect(self.bold_rect)
            """Set to None, since we only need to do this
            once."""
            self.bold_rect = None
        else:
            rect_return = self.rect
        #Draw the text
        screen.blit(self.image, self.rect)
        #Dirty no more
        self.dirty = False

    return rect_return

Finally we get to the draw code, and thankfully it’s pretty simple. The draw() function take a pygame.Surface as the only parameter, this is the surface that we will be drawing to, probably the main surface in your game or application. The draw function will also return a pygame.Rect object (or None) which represents the area of the surface that needs to be updated (or not).

The first thing we do in the function is make sure that we actually have something to draw. We check our rectangle (self.tect) and our surface (self.image) to make sure that everything has been initialized, and then we check self.dirty to see if we should draw anything.

Then we check to see if self.bold_rect is valid, if it is we return it as the rectangle to update and invalidate the bold_rect, since it only needed once, when moving from highlighted to non-highlighted). If self.bold_rect is not valid then we will return self.rect as the rectangle to update.

Then we blit our text to the screen (which is stored in self.image as a pygame.Surface object) and set self.dirty to False so that we do not needlessly draw in the future.

Events

If you remember way back at the beginning of this tutorial we create a define for a future event that would be fired when the TextWidget was clicked on:

TEXT_WIDGET_CLICK = pygame.locals.USEREVENT + 1

Now is where we use it and let the main class know when a TextWidget has been clicked.

def on_mouse_button_down(self, event):
    """Called by the main application when the
    MOUSEBUTTONDOWN event fires.
    @param event - Pygame Event object
    MOUSEBUTTONDOWN  pos, button
    """
    #Check for collision
    self.tracking = False
    if (self.rect.collidepoint(event.pos)):
        self.tracking = True

First we have the on_mouse_button_down() function, this should be called by your main application during the event loop. If you come across a MOUSEBUTTONDOWN event, then you should pass it to all of your TextWidget objects, or at least the TextWidgets that you want to be interactive.

All this function does is check to see if the mouse button down event occurred within the TextWidgets rect, if it did, self.Tracking is set to True to let the TextWidget know that it is “tracking” or listening for the MOUSEBUTTONUP event to complete the click.

def on_mouse_button_up(self, event):
    """Called by the main application when the
    MOUSEBUTTONDOWN event fires.
    @param event - Pygame Event object
    MOUSEBUTTONDOWN  pos, button
    """
    #Check for collision
    if ((self.tracking) and (self.rect.collidepoint(event.pos))):
        self.on_mouse_click(event)
    #Not Tracking anymore
    self.tracking = False

Next we have the on_mouse_button_up function which called by your main application for all of your interactive TextWidget objects when the MOUSEBUTTONDOWN event is found.

This function is very simple, it first checks to see if we are currently tracking, if we are then we check to see if the event has occurred within the TextWidget’s rect. If it has we call self.one_mouse_click() and pass it the event.

Then we set self.tracking to False, since whatever happened we are no longer tracking. Now if you don’t fully understand what the self.tracking boolean does, it simply makes sure that the TextWidget behaves like a standard button, which means that it only registers MOUSEBUTTONUP events as a “click” if the MOUSEBUTTONDOWN also occurred within the buttons rectangle.

def on_mouse_click(self, event):
    """Called by the main application when the
    MOUSEBUTTONDOWN event fires, and the text widget
    has been clicked on.  You can either let
    this post the event (default) or you can override this
    function call in your app.
    ie. myTextWidget.on_mouse_click = my_click_handler
    @param event - Pygame Event object
    MOUSEBUTTONDOWN  pos, button
    """
    #Create the TEXT_WIDGET_CLICK event
    event_attrib = {}
    event_attrib["button"] = event.button
    event_attrib["pos"] = event.pos
    event_attrib["text_widget"] = self
    e = pygame.event.Event(TEXT_WIDGET_CLICK, event_attrib)
    pygame.event.post(e)

Finally in our event handling we have the on_mouse_click() function. By default this function will post a TEXT_WIDGET_CLICK event to the PyGame event queue which you can handle in whichever manner you like.

The event has the following attributes:

  • button – The button of the mouse that was used for the click. This is the same as the MOUSEBUTTONUP event’s button attribute.
  • pos (x,y) – The position of the mouse cursor when the click occurred. The is the same as the MOUSEBUTTONUP event’s pos attribute.
  • text_widget – The TextWidget object that was clicked on, so you know how to handle the event.

That is the default way that the on_mouse_click() function functions, in general it is probably easier to override the on_mouse_click() function in all of the TextWidget instances that you want to be interactive.

As an example, in your main application you might use the following for an Exit TextWidget:

self.exit_text.on_mouse_click = self.on_exit_clicked

Using the TextWidget

In order to make it easy to see how to use the TextWidget object (if it is not already) I whipped up a quick sample application, the short flash capture is to show you what it looks like without having to run it:


import os, sys
import pygame
from pygame.locals import *

import TextWidget

class PyGameText:
    """An example class used to illustrate possible ways to work
    with the TextWidget text in a pygame game.
    """
    
    def __init__(self, width=640, height=480):
        """Initialize
        @param width=640 - The width of the pygame window
        @param height=480 - The height of the pygame window.
        """
        
        pygame.init()
        #create the screen
        self.screen = pygame.display.set_mode((width, height), 0) 
        self.background = pygame.Surface(self.screen.get_size(), SWSURFACE)
        self.background = self.background.convert()
        image, rect = self.load_image("background.png")
        if (image):
            self.background.blit(image, (0,0))
        else:
            #Just fill with a solid colour
            self.background.fill((124,124,124))
        self.screen.blit(self.background, (0,0))
        
    def main_loop(self):
        """The main Python loop"""
        
        pygame.display.update()
        self.timer = pygame.time.Clock()
        
        # Text Widget list
        self.text_widgets = []
        #Create our Text WIdgets
        self.new_game_text = TextWidget.TextWidget("New Game", (200,200,200))
        #make this the biggest text
        self.new_game_text.size = 96
        self.new_game_text.rect.center = self.screen.get_rect().center
        self.new_game_text.rect.top = 0;
        self.text_widgets.append(self.new_game_text)
        
        self.high_score_text = TextWidget.TextWidget("High Scores", (255,0,0), 64)
        self.high_score_text.rect.center = self.screen.get_rect().center
        self.high_score_text.rect.top = self.new_game_text.rect.bottom + 30
        self.text_widgets.append(self.high_score_text)
        
        self.website_text = TextWidget.TextWidget("Website", (255,0,255), 64)
        self.website_text.rect.center = self.screen.get_rect().center
        self.website_text.rect.top = self.high_score_text.rect.bottom + 30
        self.text_widgets.append(self.website_text)
        
        # Different font for the last one, and let's make it increase
        # more
        self.exit_text = TextWidget.TextWidget("Exit", (0, 255,255)
                                , 64, 40
                                , pygame.font.match_font("sans", False, True))
        self.exit_text.rect.center = self.screen.get_rect().center
        self.exit_text.rect.top = self.website_text.rect.bottom + 30
        self.exit_text.on_mouse_click = self.on_exit_clicked
        self.text_widgets.append(self.exit_text)
           
        while 1: 
            #Tick of the timer
            self.timer.tick()
            
            self.event_loop()
            
            self.draw()
                
    def event_loop(self):
        """Perform the event loop."""
        
        for event in pygame.event.get():
            if (event.type == pygame.QUIT):
                pygame.quit()
                sys.exit()
            elif (event.type == ACTIVEEVENT):
                if (event.gain == 1):
                    for text in self.text_widgets:
                        text.dirty = True
                    self.draw()
                elif (event.state ==2): 
                    #We are hidden so wait for the next event
                    pygame.event.post(pygame.event.wait())             
            elif (event.type == pygame.MOUSEMOTION):
                for text in self.text_widgets:
                    text.highlight = text.rect.collidepoint(event.pos)
            elif (event.type == pygame.MOUSEBUTTONDOWN):
                for text in self.text_widgets:
                    text.on_mouse_button_down(event)
            elif (event.type == pygame.MOUSEBUTTONUP):
                for text in self.text_widgets:
                    text.on_mouse_button_up(event)
            elif (event.type == TextWidget.TEXT_WIDGET_CLICK):
                print event.text_widget             
    
    def draw(self):
        """Draw everything"""        
        rects = []
        rects.append(self.timer_update())
        for text in self.text_widgets:
            rect = text.draw(self.screen)
            if (rect):
                rects.append(rect)
        pygame.display.update(rects)        
        
    def timer_update(self):
        """Update the Timer
        returns - pygame.rect - The rect that the timer
        needs to be redrawn, or None on error"""
        
        rect_return = None
        
        if (pygame.font):     
            timer_string = "%.2f" % self.timer.get_fps()
            #basic font
            font = pygame.font.Font(None, 36)
            message = font.render(timer_string, 1, (147, 1, 16))
            if (message):
                rect_return = message.get_rect(left=0)
                rect_return.width += 25
                self.screen.blit(self.background, rect_return)
                self.screen.blit(message, rect_return) 
        
        return rect_return
    
    def load_image(self, name, colorkey=None):
        full_path = os.path.realpath(os.path.dirname(sys.argv[0]))
        full_path = os.path.join(full_path, name)
        try:
            image = pygame.image.load(full_path)
        except pygame.error, message:
            print 'Cannot load image:', full_path
            return None, None
        image = image.convert()
        if colorkey is not None:
            if colorkey is -1:
                colorkey = image.get_at((0,0))
            image.set_colorkey(colorkey, RLEACCEL)
        return image, image.get_rect()
    
    def on_exit_clicked(self, event):
        pygame.quit()
        sys.exit()

if __name__ == "__main__":
    text = PyGameText()
    text.main_loop()

Now most of the code isn’t that exciting and is a pretty basic example of a short PyGame application so I won’t explain every line of this code. I will highlight three portions:

Text Widget Creation

Here is how I created the four TextWidgets in the example application, each is slightly different so you might want to look at each and try to understand what it is doing:

# Text Widget list
self.text_widgets = []
#Create our Text WIdgets
self.new_game_text = TextWidget.TextWidget("New Game", (200,200,200))
#make this the biggest text
self.new_game_text.size = 96
self.new_game_text.rect.center = self.screen.get_rect().center
self.new_game_text.rect.top = 0;
self.text_widgets.append(self.new_game_text)

self.high_score_text = TextWidget.TextWidget("High Scores", (255,0,0), 64)
self.high_score_text.rect.center = self.screen.get_rect().center
self.high_score_text.rect.top = self.new_game_text.rect.bottom + 30;
self.text_widgets.append(self.high_score_text)

self.website_text = TextWidget.TextWidget("Website", (255,0,255), 64)
self.website_text.rect.center = self.screen.get_rect().center
self.website_text.rect.top = self.high_score_text.rect.bottom + 30;
self.text_widgets.append(self.website_text)

# Different font for the last one, and let's make it increase
# more
self.exit_text = TextWidget.TextWidget("Exit", (0, 255,255)
                        , 64, 40
                        , pygame.font.match_font("sans", False, True))
self.exit_text.rect.center = self.screen.get_rect().center
self.exit_text.rect.top = self.website_text.rect.bottom + 30;
# Override the on_mouse_click function
self.exit_text.on_mouse_click = self.on_exit_clicked
self.text_widgets.append(self.exit_text)

In general this code is pretty simple, it mostly involves creating our TextWidget objects, settings some of their properties and then adding them to a list. It really should be pretty self explanatory how this code works, one thing that I will point out is how the in the exit_text TextWidget the pygame.font.match_font() function is used to get the path to the font, and the on_mouse_click() function is overridden.

The Event Loop

def event_loop(self):
    """Perform the event loop."""

for event in pygame.event.get():
    if (event.type == pygame.QUIT):
        pygame.quit()
        sys.exit()
    elif (event.type == ACTIVEEVENT):
        if (event.gain == 1):
            for text in self.text_widgets:
                text.dirty = True
            self.draw()
        elif (event.state ==2): 
            #We are hidden so wait for the next event
            pygame.event.post(pygame.event.wait())             
    elif (event.type == pygame.MOUSEMOTION):
        for text in self.text_widgets:
            text.highlight = text.rect.collidepoint(event.pos)
    elif (event.type == pygame.MOUSEBUTTONDOWN):
        for text in self.text_widgets:
            text.on_mouse_button_down(event)
    elif (event.type == pygame.MOUSEBUTTONUP):
        for text in self.text_widgets:
            text.on_mouse_button_up(event)
    elif (event.type == TextWidget.TEXT_WIDGET_CLICK):
        print event.text_widget

The first thing you should notice is that we respond to the ACTIVEEVENT, we do this so that when the PyGame window gets focus back (after loosing it) we redraw all of the text widgets. We force the TextWidgets to redraw by setting the dirty member to True.

Next we set the TextWidget.highlight property by checking for a collision with each TextWidget rect when we get the MOUSEMOTION event.

If you want your TextWidgets to be responsive to a click whenever you get a MOUSEBUTTONDOWN or MOUSEBUTTONUP event you can simply pass it forward to the on_mouse_button_down() and on_mouse_button_up() functions in each widget, the TextWidget will take care of the rest of the processing.

The example also shows how to handle the TEXT_WIDGIT_CLICK event:

elif (event.type == TextWidget.TEXT_WIDGET_CLICK):
        print event.text_widget

In the example I simply print out the TextWidget, but you could do whatever you wanted to.

The Draw Loop

def draw(self):
    """Draw everything"""        
    rects = []
    rects.append(self.timer_update())
    for text in self.text_widgets:
        rect = text.draw(self.screen)
        if (rect):
            rects.append(rect)
    pygame.display.update(rects)

The draw loop is actually pretty simple. I use a list to keep track of rectangles that I want to update at the end of the draw using pygame.display.update() function.

In order to draw the TextWidget’s I simply loop through the list of them and call their draw() function. If the draw() function returns a rectangle, then I know that the TextWdiget has drawn so I add it to the list of rectangles that need to be updated.

If None is returned then I know that the TextWidget has not drawn and that it does not need to be drawn.

Conclusion

The full source and necessary files for this tutorial can be downloaded here.

So that’s it for this tutorial, as I said early on I’m not saying that this is the best or only way to work with text, I’m simply presenting this as a possible way to work with text, or as an example that might help get you started with your own text processing.

If you have any suggestions, problems, or notice anything that is incorrect, as always, add a comment!

Feel free to use this in whatever manner you want (it’s licensed under the LGPL), but if you do end up using it, I would appreciate it if you sent me an email and let me know.

selsine

del.icio.us del.icio.us

7 Responses to “TextWidget – A simple text class for PyGame”

  1. Phelerox
    Says:

    Useful tutorial! Very appreciated! :)

  2. Sasayaki
    Says:

    Hey mate,

    I’m using your TextWidget class for a little project of mine- it’s proving very useful!

    However, I’m getting a strange error. I’m developing on Ubuntu and the code seems to work fine- visually at least- it looks like it’s working. However after a moment or two (5->15 sec) it crashes and says:

    File “/home/sasayaki/Python/TextWidget.py”, line 163, in __init__
    File “/home/sasayaki/Python/TextWidget.py”, line 98, in __set_font_filename
    File “/home/sasayaki/Python/TextWidget.py”, line 183, in create_font
    IOError: unable to read font filename

    The text widget is created using:

    blah = TextWidget.TextWidget(“blah”, (255,0,0),font_filename=”VeraBd.ttf”)

    And VeraBd.ttf is in the same directory as the rest of the code.

    Note that it reads and displays the font just fine (as far as I can see). Anyone else getting a similar error?

  3. learning python » Blog Archive » TextWidget 0.1
    Says:

    [...] been a long time since I worked on TextWidget at all, but since someone posted a question about it I decided to fix the issue and re-release the [...]

  4. selsine

    selsine
    Says:

    Hi Sasayki,

    I’ve updated TextWidget to hopefully fix this bug, take a look at the update post or it’s new google code home: http://code.google.com/p/textwidget/

    I hope this helps.

  5. TextWidget for PyGame | OMGWTF GAMES !!1!
    Says:

    [...] to self: Check out TextWidget before I finish my (unreleased) SimpleMenu module  … I may be able to use TextWidget within [...]

  6. Sasayaki
    Says:

    Thanks Selsine, I’ll download it now and give it a try!

  7. Sasayaki
    Says:

    Hey mate,

    I downloaded the new version and gave it a spin- unfortunately it seems to still throw the same error. The update delays the time before the error is thrown, but unfortunately on my system it still throws it. I’ve created an issue on the Google Code group with more detailed information.

    Thanks for all your help with this- it’s invaluable!

Leave a Reply

 

Popular Posts