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).

Figure 10: The checkerboard and a few checkers. Append more coordinates to the black and red checkers lists to draw additional checkers.

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

Express-Checkout as PDF
Price $2.95
(incl. VAT)

Buy Raspberry Pi Geek

SINGLE ISSUES
 
SUBSCRIPTIONS
 
TABLET & SMARTPHONE APPS
Get it on Google Play

US / Canada

Get it on Google Play

UK / Australia

Related content