Introduction

This recipe provides a simple double buffered window that can be sub-classed to do any customized drawing you might need, without worrying about Paint events, etc.

What Is Double Buffering?

Double buffering is storing the contents of a window in memory (the buffer), so that the screen can be easily refreshed without having to re-draw the whole thing.

Why double Buffer?

Whenever a window displayed on the screen gets damaged, by, for instance, a dialog box being displayed over it, the system asks the program to re-draw it. This is indicated by a Paint event, and is handled in a Paint event handler. Usually the Paint event handler creates a wx.PaintDC, and calls code that re-draws the window. See CustomisedDrawing and WxHowtoDrawing for an example, as well as assorted code in the wxPython demo.

This works fine for simple drawings, but if you have a complex drawing, it can take a while for the program to draw, and the result is that the user has to sit there and wait, while they watch the screen re-draw itself. Personally, I find it really annoying to watch a window slowly redraw itself, just because I moved a dialog box or something.

Double buffering solves this problem, because all a window needs to do to re-draw itself is to copy the buffer to the screen. This is a very fast operation.

What Objects are Involved

This recipe will help you figure out how to use a number of DeviceContexts (DCs), including:

See CustomisedDrawing for more on using DC's in general.

This recipe also covers the basics of using a

Process Overview

The basic process is to create a class derived from wx.Window, that keeps a wx.Bitmap around with a copy of the image on the screen. The OnPaint handler simply copies that bitmap to the screen, and when the image needs to be updated, it is drawn to the bitmap, and the bitmap is re-copied to the screen.

The Bufferedwindow class

wxBufferedWindowFlow

(warning: drawing not yet updated to the wx namespace)

   1 import wx
   2 import random

Import the usual wx module.

Import the random module, too; We'll need it for random data later in the demo.

   1 USE_BUFFERED_DC = 1

This example can optionally use the wx.BufferedDC.

If USE_BUFFERED_DC is true, it will be used. Otherwise, the program uses the raw wx.MemoryDC, etc.

The wx.BufferedDC is a relatively recent addition that makes it a little easier to double buffer.

With this switch, you can see how it works, both ways.

   1 class BufferedWindow(wx.Window):
   2     def __init__(self, parent, id,
   3                  pos = wx.DefaultPosition,
   4                  size = wx.DefaultSize,
   5                  style=wx.NO_FULL_REPAINT_ON_RESIZE):
   6         wx.Window.__init__(self, parent, id, pos, size, style)
   7 
   8         self.Bind(wx.EVT_PAINT, self.OnPaint)
   9         self.Bind(wx.EVT_SIZE, self.OnSize)
  10 
  11         self.OnSize(None)

This is the class init function. The class subclasses from wx.Window.

The init binds a couple events.

Note the style: wx.NO_FULL_REPAINT_ON_RESIZE. This style can reduce flickering when resizing windows on Microsoft Windows. I'm not sure it makes a difference on other platforms (thanks to KevinAltis for that hint).

self.OnSize() is called lastly. It's called last, because OnSize will initialize the buffer, and we want to make sure it's the right size. It may end up getting called twice during initialization on some platforms, but there is little harm done.

   1     def Draw(self, dc):
   2         pass

Draw() is just here as a place holder.

You'll override it in sub-classes.

It will inspect your system, find out what needs to be drawn, and then draw it.

UpdateDrawing will call Draw. UpdateDrawing will take care of boring details, to make sure that Draw only has to work with a nice drawing context.

This function, Draw, is responsible for all of your drawing code. It is responsible for the scene creation. No other functions will be drawing on the buffer; only this one.

   1     def OnSize(self, event):
   2         # The Buffer is initialized in OnSize, so that the buffer is always
   3         # the same size as the Window.
   4         self.Width, self.Height = self.GetClientSizeTuple()
   5         # Make new off screen bitmap: this bitmap will always have the
   6         # current drawing in it, so it can be used to save the image to
   7         # a file, or whatever.
   8         self._Buffer = wx.EmptyBitmap(self.Width, self.Height)
   9         self.UpdateDrawing()

The OnSize() Method.

When the window is resized, or on first init, this is called.

It's responsible for:

OnSize will fill the first (making the buffer the right size,) and then delegating the other two to UpdateDrawing.

When OnSize creates the buffer, it's completely blank. UpdateDrawing will fill it (with the help of Draw,) and then blit the buffer to the screen.

UpdateDrawing is a method that we invented, for our convenience; it's not a built-in part of wxPython.

Does OnSize trigger a call to OnPaint? I don't think so. UpdateDrawing will make sure that the buffer is drawn to (via Draw()), and UpdateDrawing will make sure that the buffer is drawn to the screen.

   1     def OnPaint(self, event):
   2         # All that is needed here is to draw the buffer to screen
   3         if USE_BUFFERED_DC:
   4             dc = wx.BufferedPaintDC(self, self._Buffer)
   5         else:
   6             dc = wx.PaintDC(self)
   7             dc.DrawBitmap(self._Buffer,0,0)

Now the OnPaint() method. It's called whenever something gets dirty.

Since the image is stored in the buffer, all we have to do here is copy the buffer to the screen.

Ideally, you'd only copy the damaged region to the screen. But I've found I can't tell the difference, so I don't bother. In fact, on Windows at least, only the damaged region is copied, anyways, with this code.

There are two approaches here, depending on USE_BUFFERED_DC.

USE_BUFFERED_DC is True.

This way is a little easier.

The wx.BufferedPaintDC takes a bitmap as input (self._Buffer,) and creates a DC that you can use to draw to the bitmap. We're not going to draw to the bitmap; Our theory is that we've already drawn to the bitmap. So all we do is create the wx.BufferedPaintDC.

Now the object is somewhat unique, in that the bitmap is copied to the screen when the object goes out of scope. That is, when the OnPaint method is done, the object is no longer needed, and it is deleted. Just before it's deleted, though, it copies itself to the screen.

USE_BUFFERED_DC is False.

Without the BufferedPaintDC, the process is similar, except that we have to make a call to copy the bitmap to the wx.PaintDC.

Note that we use dc.DrawBitmap(), rather than dc.Blit(). DrawBitmap accomplishes assentially the same thing, but with an easier interface, for our purpose here.

   1     def UpdateDrawing(self):
   2         if USE_BUFFERED_DC:
   3             dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
   4             self.Draw(dc)
   5         else:
   6             # update the buffer
   7             dc = wx.MemoryDC()
   8             dc.SelectObject(self._Buffer)
   9 
  10             self.Draw(dc)
  11             # update the screen
  12             wx.ClientDC(self).Blit(0, 0, self.Width, self.Height, dc, 0, 0)

UpdateDrawing() is a method we've invented here, that you should call whenever the drawing needs to update. The drawing is generated from data found elsewhere in the system. If that data change, the drawing needs to be updated, so be sure to call this function.

This function does all the pre-drawing and post-drawing work. The Draw function we created earlier, does the actual drawing work.

USE_BUFFERED_DC is True.

The wx.BufferedDC is passed to the Draw() function to be drawn on.

As in the OnPaint implementation, the drawing context will be automatically blit to the DC that was passed in as an argument (wx.ClientDC(self)) when it is destroyed / goes out of scope.

(wx.ClientDC is used to draw to a window outside of a Paint event.

USE_BUFFERED_DC is False.

A wx.MemoryDC is created to draw to the bitmap with. You have to remember: a bitmap is not a drawing context! You can't use our neat drawing context functions, when all you hold is a bitmap. You have to actually put the bitmap into the drawing context, and then you can do your neat drawing things on it.

When the Draw method is done, the contents of our drawing context (the bitmap) are blit to the wx.ClientDC.

Why DC.Blit, rather than DC.DrawBitmap? On Windows, the screen was sometimes not updated properly with the DC.DrawBitmap call. Blit seemed to be more reliable. (This may have been a bug in wxMSW, which stands for: "wx-Microsoft-Windows." Maybe it's fixed now. Maybe it wasn't a bug, and I just don't understand something.)

   1     def SaveToFile(self,FileName,FileType):
   2         ## This will save the contents of the buffer
   3         ## to the specified file. See the wx.Windows docs for
   4         ## wx.Bitmap::SaveFile for the details
   5         self._Buffer.SaveFile(FileName,FileType)

The next method saves the contents of the buffer to a file. The buffer always contains the same image as the screen, so this makes it very easy to take a screenshot.

Using the BufferedWindow class

In order to use the BufferedWindow class, you must create a subclass, and define a Draw() method appropriate to your application.

The init method is very straight forward. Note that any data needed by your Draw() method must be defined before calling BufferedWindow.__init__(), because the Draw() method is called when it is initialized. In this case, I have created an empty dictionary that will cause the Draw() method to do nothing.

   1 class DrawWindow(BufferedWindow):
   2     def __init__(self, parent, id = -1):
   3         ## Any data the Draw() function needs must be initialized before
   4         ## calling BufferedWindow.__init__, as it will call the Draw
   5         ## function.
   6 
   7         self.DrawData = {}
   8         BufferedWindow.__init__(self, parent, id)

Here is the Draw() method. In this case, it examines a dictionary of data, and creates the drawing defined by that data. The data itself is generated by the application using this window.

   1     def Draw(self, dc):
   2         dc.BeginDrawing()
   3         dc.SetBackground( wx.Brush("White") )
   4         dc.Clear() # make sure you clear the bitmap!
   5 
   6         # Here's the actual drawing code.
   7         for key,data in self.DrawData.items():
   8             if key == "Rectangles":
   9                 dc.SetBrush(wx.BLUE_BRUSH)
  10                 dc.SetPen(wx.Pen('VIOLET', 4))
  11                 for r in data:
  12                     dc.DrawRectangle(*r)
  13             elif key == "Ellipses":
  14                 dc.SetBrush(wx.Brush("GREEN YELLOW"))
  15                 dc.SetPen(wx.Pen('CADET BLUE', 2))
  16                 for r in data:
  17                     dc.DrawEllipse(*r)
  18             elif key == "Polygons":
  19                 dc.SetBrush(wx.Brush("SALMON"))
  20                 dc.SetPen(wx.Pen('VIOLET RED', 4))
  21                 for r in data:
  22                     dc.DrawPolygon(r)
  23         dc.EndDrawing()

Using the newly defined buffered Window.

Next is a sample application that uses the buffered draw window defined above.

A main frame for the app, with a simple menu bar:

   1 class TestFrame(wx.Frame):
   2     def __init__(self):
   3         wx.Frame.__init__(self, None, -1, "Double Buffered Test",
   4                          wx.DefaultPosition,
   5                          size=(500,500),
   6                          style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE)
   7 
   8         ## Set up the MenuBar
   9         MenuBar = wx.MenuBar()
  10 
  11         file_menu = wx.Menu()
  12         ID_EXIT_MENU = wx.NewId()
  13         file_menu.Append(ID_EXIT_MENU, "E&xit","Terminate the program")
  14         self.Bind(wx.EVT_MENU, self.OnQuit, id=ID_EXIT_MENU)
  15         MenuBar.Append(file_menu, "&File")
  16 
  17         draw_menu = wx.Menu()
  18         ID_DRAW_MENU = wx.NewId()
  19         draw_menu.Append(ID_DRAW_MENU, "&New Drawing","Update the Drawing Data")
  20         self.Bind(wx.EVT_MENU, self.NewDrawing, id=ID_DRAW_MENU)
  21         BMP_ID = wx.NewId()
  22         draw_menu.Append(BMP_ID,'&Save Drawing\tAlt-I','')
  23         self.Bind(wx.EVT_MENU, self.SaveToFile, id=BMP_ID)
  24         MenuBar.Append(draw_menu, "&Draw")
  25 
  26         self.SetMenuBar(MenuBar)
  27 
  28         self.Window = DrawWindow(self)
  29 
  30     def OnQuit(self,event):
  31         self.Close(True)
  32 
  33     def NewDrawing(self,event):
  34         self.Window.DrawData = self.MakeNewData()
  35         self.Window.UpdateDrawing()
  36 
  37     def SaveToFile(self,event):
  38         dlg = wx.FileDialog(self, "Choose a file name to save the image as a PNG to",
  39                            defaultDir = "",
  40                            defaultFile = "",
  41                            wildcard = "*.png",
  42                            style=wx.SAVE)
  43         if dlg.ShowModal() == wx.ID_OK:
  44             self.Window.SaveToFile(dlg.GetPath(),wx.BITMAP_TYPE_PNG)
  45         dlg.Destroy()
  46 
  47     def MakeNewData(self):
  48         ## This method makes some random data to draw things with.
  49         MaxX, MaxY = self.Window.GetClientSizeTuple()
  50         #MaxX = 500
  51         #MaxY = 500
  52         DrawData = {}
  53 
  54         # make some random rectangles
  55         l = []
  56         for i in range(5):
  57             w = random.randint(1,MaxX/2)
  58             h = random.randint(1,MaxY/2)
  59             x = random.randint(1,MaxX-w)
  60             y = random.randint(1,MaxY-h)
  61             l.append( (x,y,w,h) )
  62         DrawData["Rectangles"] = l
  63 
  64         # make some random ellipses
  65         l = []
  66         for i in range(5):
  67             w = random.randint(1,MaxX/2)
  68             h = random.randint(1,MaxY/2)
  69             x = random.randint(1,MaxX-w)
  70             y = random.randint(1,MaxY-h)
  71             l.append( (x,y,w,h) )
  72         DrawData["Ellipses"] = l
  73 
  74         # Polygons
  75         l = []
  76         for i in range(3):
  77             points = []
  78             for j in range(random.randint(3,8)):
  79                 point = (random.randint(1,MaxX),random.randint(1,MaxY))
  80                 points.append(point)
  81             l.append(points)
  82         DrawData["Polygons"] = l
  83 
  84         return DrawData

A simple wx.App to create the frame.

   1 class DemoApp(wx.App):
   2     def OnInit(self):
   3         wx.InitAllImageHandlers() # called so a PNG can be saved
   4         frame = TestFrame()
   5         frame.Show(True)
   6 
   7         ## initialize a drawing
   8         ## It doesn't seem like this should be here, but the Frame does
   9         ## not get sized until Show() is called, so it doesn't work if
  10         ## it is put in the __init__ method.
  11         frame.NewDrawing(None)
  12 
  13         self.SetTopWindow(frame)
  14 
  15         return True
  16 
  17 if __name__ == "__main__":
  18     app = DemoApp(0)
  19     app.MainLoop()

Special Concerns

If your image is big, you may want to have some way to scroll around it. One method is to use a wx.ScrolledWindow, but remember that the buffer has to be updated as you scroll, which can be pretty slow. Another method is to have a really big bitmap, and just blit the appropriate part to the screen as you scroll. This works well, but can only accommodate a moderate sized bitmap. Memory use can get big very fast!

Code Sample

Here's all the code in one piece, so that you can try the sample app:

   1 # -*- coding: iso-8859-1 -*-#
   2 #!/usr/bin/env python2.4
   3 
   4 import wx
   5 import random
   6 
   7 # This has been set up to optionally use the wx.BufferedDC if
   8 # USE_BUFFERED_DC is True, it will be used. Otherwise, it uses the raw
   9 # wx.Memory DC , etc.
  10 
  11 USE_BUFFERED_DC = 1
  12 
  13 class BufferedWindow(wx.Window):
  14 
  15     """
  16 
  17     A Buffered window class.
  18 
  19     To use it, subclass it and define a Draw(DC) method that takes a DC
  20     to draw to. In that method, put the code needed to draw the picture
  21     you want. The window will automatically be double buffered, and the
  22     screen will be automatically updated when a Paint event is received.
  23 
  24     When the drawing needs to change, you app needs to call the
  25     UpdateDrawing() method. Since the drawing is stored in a bitmap, you
  26     can also save the drawing to file by calling the
  27     SaveToFile(self,file_name,file_type) method.
  28 
  29     """
  30 
  31 
  32     def __init__(self, parent, id,
  33                  pos = wx.DefaultPosition,
  34                  size = wx.DefaultSize,
  35                  style = wx.NO_FULL_REPAINT_ON_RESIZE):
  36         wx.Window.__init__(self, parent, id, pos, size, style)
  37 
  38         wx.EVT_PAINT(self, self.OnPaint)
  39         wx.EVT_SIZE(self, self.OnSize)
  40 
  41 
  42         # OnSize called to make sure the buffer is initialized.
  43         # This might result in OnSize getting called twice on some
  44         # platforms at initialization, but little harm done.
  45         self.OnSize(None)
  46 
  47     def Draw(self,dc):
  48         ## just here as a place holder.
  49         ## This method should be over-ridden when subclassed
  50         pass
  51 
  52     def OnPaint(self, event):
  53         # All that is needed here is to draw the buffer to screen
  54         if USE_BUFFERED_DC:
  55             dc = wx.BufferedPaintDC(self, self._Buffer)
  56         else:
  57             dc = wx.PaintDC(self)
  58             dc.DrawBitmap(self._Buffer,0,0)
  59 
  60     def OnSize(self,event):
  61         # The Buffer init is done here, to make sure the buffer is always
  62         # the same size as the Window
  63         Size  = self.GetClientSizeTuple()
  64 
  65         # Make new offscreen bitmap: this bitmap will always have the
  66         # current drawing in it, so it can be used to save the image to
  67         # a file, or whatever.
  68         self._Buffer = wx.EmptyBitmap(*Size)
  69         self.UpdateDrawing()
  70 
  71     def SaveToFile(self,FileName,FileType):
  72         ## This will save the contents of the buffer
  73         ## to the specified file. See the wxWindows docs for 
  74         ## wx.Bitmap::SaveFile for the details
  75         self._Buffer.SaveFile(FileName,FileType)
  76 
  77     def UpdateDrawing(self):
  78         """
  79         This would get called if the drawing needed to change, for whatever reason.
  80 
  81         The idea here is that the drawing is based on some data generated
  82         elsewhere in the system. If that data changes, the drawing needs to
  83         be updated.
  84 
  85         """
  86 
  87         if USE_BUFFERED_DC:
  88             dc = wx.BufferedDC(wx.ClientDC(self), self._Buffer)
  89             self.Draw(dc)
  90         else:
  91             # update the buffer
  92             dc = wx.MemoryDC()
  93             dc.SelectObject(self._Buffer)
  94             self.Draw(dc)
  95             # update the screen
  96             wx.ClientDC(self).DrawBitmap(self._Buffer,0,0)
  97 
  98 class DrawWindow(BufferedWindow):
  99     def __init__(self, parent, id = -1):
 100         ## Any data the Draw() function needs must be initialized before
 101         ## calling BufferedWindow.__init__, as it will call the Draw
 102         ## function.
 103 
 104         self.DrawData = {}
 105         BufferedWindow.__init__(self, parent, id)
 106 
 107     def Draw(self, dc):
 108         dc.SetBackground( wx.Brush("White") )
 109         dc.Clear() # make sure you clear the bitmap!
 110 
 111         # Here's the actual drawing code.
 112         for key,data in self.DrawData.items():
 113             if key == "Rectangles":
 114                 dc.SetBrush(wx.BLUE_BRUSH)
 115                 dc.SetPen(wx.Pen('VIOLET', 4))
 116                 for r in data:
 117                     dc.DrawRectangle(*r)
 118             elif key == "Ellipses":
 119                 dc.SetBrush(wx.Brush("GREEN YELLOW"))
 120                 dc.SetPen(wx.Pen('CADET BLUE', 2))
 121                 for r in data:
 122                     dc.DrawEllipse(*r)
 123             elif key == "Polygons":
 124                 dc.SetBrush(wx.Brush("SALMON"))
 125                 dc.SetPen(wx.Pen('VIOLET RED', 4))
 126                 for r in data:
 127                     dc.DrawPolygon(r)
 128 
 129 
 130 class TestFrame(wx.Frame):
 131     def __init__(self):
 132         wx.Frame.__init__(self, None, -1, "Double Buffered Test",
 133                          wx.DefaultPosition,
 134                          size=(500,500),
 135                          style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE)
 136 
 137         ## Set up the MenuBar
 138         MenuBar = wx.MenuBar()
 139 
 140         file_menu = wx.Menu()
 141         ID_EXIT_MENU = wx.NewId()
 142         file_menu.Append(ID_EXIT_MENU, "E&xit","Terminate the program")
 143         wx.EVT_MENU(self, ID_EXIT_MENU, self.OnQuit)
 144         MenuBar.Append(file_menu, "&File")
 145 
 146         draw_menu = wx.Menu()
 147         ID_DRAW_MENU = wx.NewId()
 148         draw_menu.Append(ID_DRAW_MENU, "&New Drawing","Update the Drawing Data")
 149         wx.EVT_MENU(self, ID_DRAW_MENU,self.NewDrawing)
 150         BMP_ID = wx.NewId()
 151         draw_menu.Append(BMP_ID,'&Save Drawing\tAlt-I','')
 152         wx.EVT_MENU(self,BMP_ID, self.SaveToFile)
 153         MenuBar.Append(draw_menu, "&Draw")
 154 
 155         self.SetMenuBar(MenuBar)
 156 
 157 
 158         self.Window = DrawWindow(self)
 159 
 160     def OnQuit(self,event):
 161         self.Close(True)
 162 
 163     def NewDrawing(self,event):
 164         self.Window.DrawData = self.MakeNewData()
 165         self.Window.UpdateDrawing()
 166 
 167     def SaveToFile(self,event):
 168         dlg = wx.FileDialog(self, "Choose a file name to save the image as a PNG to",
 169                            defaultDir = "",
 170                            defaultFile = "",
 171                            wildcard = "*.png",
 172                            style = wx.SAVE)
 173         if dlg.ShowModal() == wx.ID_OK:
 174             self.Window.SaveToFile(dlg.GetPath(),wx.BITMAP_TYPE_PNG)
 175         dlg.Destroy()
 176 
 177     def MakeNewData(self):
 178         ## This method makes some random data to draw things with.
 179         MaxX, MaxY = self.Window.GetClientSizeTuple()
 180         DrawData = {}
 181 
 182         # make some random rectangles
 183         l = []
 184         for i in range(5):
 185             w = random.randint(1,MaxX/2)
 186             h = random.randint(1,MaxY/2)
 187             x = random.randint(1,MaxX-w)
 188             y = random.randint(1,MaxY-h)
 189             l.append( (x,y,w,h) )
 190         DrawData["Rectangles"] = l
 191 
 192         # make some random ellipses
 193         l = []
 194         for i in range(5):
 195             w = random.randint(1,MaxX/2)
 196             h = random.randint(1,MaxY/2)
 197             x = random.randint(1,MaxX-w)
 198             y = random.randint(1,MaxY-h)
 199             l.append( (x,y,w,h) )
 200         DrawData["Ellipses"] = l
 201 
 202         # Polygons
 203         l = []
 204         for i in range(3):
 205             points = []
 206             for j in range(random.randint(3,8)):
 207                 point = (random.randint(1,MaxX),random.randint(1,MaxY))
 208                 points.append(point)
 209             l.append(points)
 210         DrawData["Polygons"] = l
 211 
 212         return DrawData
 213 
 214 class DemoApp(wx.App):
 215     def OnInit(self):
 216         #wx.InitAllImageHandlers() # called so a PNG can be saved      
 217         frame = TestFrame()
 218         frame.Show(True)
 219 
 220         ## initialize a drawing
 221         ## It doesn't seem like this should be here, but the Frame does
 222         ## not get sized until Show() is called, so it doesn't work if
 223         ## it is put in the __init__ method.
 224         frame.NewDrawing(None)
 225 
 226         self.SetTopWindow(frame)
 227 
 228         return True
 229 
 230 if __name__ == "__main__":
 231     print "about to initialize the app"
 232     app = DemoApp(0)
 233     app.MainLoop()

See Also

Comments

Please give this code a thorough review. I'd like to see it evolve into a generally useful class, rather than just a demo.

- Chris Barker 1/27/03 Chris.Barker@noaa.gov

DoubleBufferedDrawing (last edited 2008-03-11 10:50:17 by localhost)