Graphical displays with Python and Pygame
Refreshing Sections of the Screen
Refreshing the entire screen takes a long time, especially on low-power platforms, but that's not to say you should never use flip()
. If I set up a full-screen background or subdivide my entire window with lines, I can call flip()
at the end of my setup routine. After that, I get the best performance by refreshing only the areas that have changed.
The Python and Pygame class in Listing 4 draws a checkers board. Each member function keeps track of screen areas that it updated. To use the class, add (x, y) tuples to the lists blackCheckers
, redCheckers
, and clearList
using append
. The board origin is (0, 0) in the upper left corner (Figure 10).
Listing 4
checkers/checkers.py
01 import pygame 02 03 class checkers: 04 def __init__ ( self, screen ): 05 self.board = list() 06 self.blackCheckers = list() 07 self.redCheckers = list() 08 self.clearList = list() 09 self.refreshList = list() 10 11 blackRow = list() 12 redRow = list() 13 14 self.screen = screen 15 16 for i in range ( 4 ): 17 blackRow.append ( ( 128, 0, 0 ) ) 18 blackRow.append ( ( 10, 10, 10 ) ) 19 20 redRow.append ( ( 10, 10, 10 ) ) 21 redRow.append ( ( 128, 0, 0 ) ) 22 23 for i in range ( 4 ): 24 self.board.append ( blackRow ) 25 self.board.append ( redRow ) 26 27 def drawBoard ( self ): 28 x = 0 29 y = 0 30 31 for row in self.board: 32 for square in row: 33 pygame.draw.rect ( self.screen, square, ( x * 50, y * 50, 50, 50 ), 0 ) 34 x += 1 35 x = 0 36 y += 1 37 38 pygame.display.flip() 39 40 def drawCheckers ( self ): 41 for address in self.blackCheckers: 42 refreshRect = pygame.draw.circle ( self.screen, ( 0, 0, 0 ), ( address [ 0 ] * 50 + 25, address [ 1 ] * 50 + 25 ), 20, 0 ) 43 self.refreshList.append ( refreshRect ) 44 for address in self.redCheckers: 45 refreshRect = pygame.draw.circle ( self.screen, ( 200, 0, 0 ), ( address [ 0 ] * 50 + 25, address [ 1 ] * 50 + 25 ), 20, 0 ) 46 self.refreshList.append ( refreshRect ) 47 48 def clearSpace ( self ): 49 for address in self.clearList: 50 refreshRect = pygame.draw.rect ( self.screen, self.board [ address [ 0 ] ] [ address [ 1 ] ], ( address [ 0 ] * 50, address [ 1 ] * 50, 50, 50 ), 0 ) 51 self.refreshList.append ( refreshRect ) 52 53 def update ( self ): 54 self.clearSpace() 55 self.drawCheckers() 56 pygame.display.update ( self.refreshList ) 57 self.refreshList = list() 58 59 pygame.display.init() 60 screen = pygame.display.set_mode ( ( 640, 480 ) ) 61 62 game = checkers ( screen ) 63 64 game.drawBoard() 65 game.redCheckers.append ( ( 3, 0 ) ) 66 game.blackCheckers.append ( ( 4, 2 ) ) 67 game.clearList.append ( ( 3, 0 ) ) 68 game.clearList.append ( ( 5, 0 ) ) 69 game.update() 70 71 raw_input() 72 pygame.quit()
When you call checkers.update()
, it first clears all of the squares in clearList
by redrawing the original board color, then it draws the checkers in each checker list. The draw functions return a rect
if the screen was affected by the call. These are added to refreshList
. After all the internal drawing functions have been called, checkers.update()
passes refreshList
to pygame.update()
. Each rect
in refreshList is updated on the screen. The checkers example is somewhat simplistic, but it shows the concept of tracking screen updates. In larger programs, you might have many drawing functions that need to update the screen. In this case, an update function can call things in the proper order and refresh the screen once at the end.
Graphing
The lineGraph.py
program (Listing 5) has a graph
class that draws a scale, grid, alignment marks, labels, and line graph, primarily using drawing functions discussed previously. It also initializes the font module to draw labels. A dataFile
class reads the data to be plotted from a text file. Some values in the program are hard coded, but it could easily be converted into a full graphing module.
Listing 5
lineGraph.py
001 import pygame 002 003 class graph: 004 def __init__ ( self, screen ): 005 self.screen = screen 006 self.width = screen.get_width() 007 self.height = screen.get_height() 008 self.graphOriginY = int ( float ( self.height ) * .8 ) 009 self.graphOriginX = ( self.height - self.graphOriginY ) 010 self.labelColor = ( 0, 200, 0 ) 011 012 pygame.font.init() 013 self.labelFont = pygame.font.SysFont ( "", 20 ) 014 015 016 def drawAxes ( self ): 017 pygame.draw.line ( self.screen, self.labelColor, ( self.graphOriginX, self.graphOriginY ), ( self.graphOriginX, 0 ) ) 018 pygame.draw.line ( self.screen, self.labelColor, ( self.graphOriginX, self.graphOriginY ), ( self.width, self.graphOriginY ) ) 019 020 def drawHorizontalHash ( self ): 021 for i in range ( self.graphOriginX, self.width, 25 ): 022 pygame.draw.line ( self.screen, self.labelColor, ( i, self.graphOriginY - 5 ), ( i, self.graphOriginY + 5 ) ) 023 024 def drawVerticalHash ( self ): 025 for i in range ( self.graphOriginY, 0, -25 ): 026 pygame.draw.line ( self.screen, self.labelColor, ( self.graphOriginX - 5, i ), ( self.graphOriginX + 5, i ) ) 027 028 def drawGrid ( self ): 029 for x in range ( self.graphOriginX + 50, self.width, 50 ): 030 for y in range ( self.graphOriginY, 0, -50 ): 031 pygame.draw.line ( self.screen, self.labelColor, ( x, y ), ( x, y ) ) 032 033 def drawScale ( self ): 034 for i in range ( self.graphOriginY, 0, -50 ): 035 value = float ( i ) / float ( self.graphOriginY ) * ( self.dataMax - self.dataMin ) + self.dataMin 036 labelSurf = self.labelFont.render ( "{:5.2f}".format ( value ), True, self.labelColor ) 037 self.screen.blit ( labelSurf, ( self.graphOriginX - labelSurf.get_width() - 10, self.graphOriginY - i ) ) 038 039 def graphLine ( self, data, labels, start, end ): 040 myData = list() 041 dataMin = None 042 dataMax = None 043 graphWidth = self.width - self.graphOriginX 044 dataWidth = int ( graphWidth / 25 ) 045 for dataPoint in self.walkValues ( start, end, dataWidth ): 046 value = float ( data [ dataPoint ] ) 047 if value < dataMin or dataMin == None: dataMin = value 048 if value > dataMax or dataMax == None: dataMax = value 049 myData.append ( value ) 050 scale = float ( self.graphOriginY ) / ( dataMax - dataMin ) 051 self.dataMin = dataMin 052 self.dataMax = dataMax 053 054 points = list() 055 for index, pointVal in enumerate ( myData ): 056 points.append ( ( index * 25 + self.graphOriginX, self.graphOriginY - int ( float ( pointVal - dataMin ) * scale ) ) ) 057 labelSurf = self.labelFont.render ( labels [ index ], True, self.labelColor ) 058 labelSurf = pygame.transform.rotate ( labelSurf, 45 ) 059 self.screen.blit ( labelSurf, ( index * 25 + self.graphOriginX - labelSurf.get_width() + 5, self.graphOriginY + 5 ) ) 060 pygame.draw.lines ( self.screen, ( 250, 0, 0 ), False, points ) 061 062 def setupGraph ( self ): 063 self.drawAxes() 064 self.drawHorizontalHash() 065 self.drawVerticalHash() 066 self.drawGrid() 067 068 def walkValues ( self, start, end, size ): 069 listSize = end - start 070 sampleSpacing = int ( listSize / size ) 071 for i in range ( start, end - sampleSpacing, sampleSpacing ): 072 yield i 073 yield end - 1 074 075 076 077 class dataFile: 078 columns = list() 079 080 def __init__ ( self ): 081 pass 082 083 def loadFile ( self, fileName, separator ): 084 dataFile = file ( fileName, "r" ) 085 firstTime = True 086 for line in dataFile.readlines(): 087 lineParts = line.split ( separator ) 088 if firstTime == True: 089 for index, cell in enumerate ( lineParts ): 090 self.columns.append ( list() ) 091 self.columns [ index ].append ( cell.strip ( "\r\n" ) ) 092 firstTime = False 093 else: 094 for index, cell in enumerate ( lineParts ): 095 self.columns [ index ].append ( cell.strip ( "\r\n" ) ) 096 097 def getColumn ( self, column, columnType = None ): 098 if columnType == None: 099 return self.columns [ column ] 100 elif columnType == "INT": 101 output = list() 102 for value in self.columns [ column ]: 103 output.append ( int ( value ) ) 104 return output 105 elif columnType == "FLOAT": 106 output = list() 107 for value in self.columns [ column ]: 108 output.append ( float ( value ) ) 109 return output 110 111 def printTable ( self ): 112 columnIndex = 0 113 for i in range ( 25 ): 114 text = "" 115 for column in self.columns: 116 text += column [ i ] + "\t" 117 print text 118 119 df = dataFile() 120 df.loadFile ( "tempdata.csv", "," ) 121 122 pygame.display.init() 123 screen = pygame.display.set_mode ( ( 1024, 600 ) ) 124 125 chart = graph ( screen ) 126 chart.setupGraph() 127 128 labels = list() 129 dates = df.getColumn ( 1 ) 130 times = df.getColumn ( 2 ) 131 for i in range ( len ( dates ) ): 132 labels.append ( dates [ i ] + " " + times [ i ] ) 133 134 chart.graphLine ( df.getColumn ( 3 ), labels, 0, 8000 ) 135 chart.drawScale() 136 pygame.display.flip() 137 138 raw_input() 139 pygame.quit()
The __init__ ()
function in line 4 takes the screen
variable as an argument and then does all of the drawing internally. The rest of the variables in init
are either retrieved or calculated from the screen variable; init
finishes by initializing the font module. pygame.font.SysFont ( "", 20 )
retrieves the default system font. The 20
requests that the font be rendered 20 pixels tall.
If you have a TTF font you want to use, you can load it with pygame.font.Font
. Its arguments are the same as SysFont
, except the first argument is the TTF file to load.
Buy this article as PDF
(incl. VAT)