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:
wx.PaintDC -- drawing to the screen, during EVT_PAINT
wx.ClientDC -- drawing to the screen, outside EVT_PAINT
wx.BufferedPaintDC -- drawing to a buffer, then the screen, during EVT_PAINT
wx.BufferedDC -- drawing to a buffer, then the screen, outside EVT_PAINT
wx.MemoryDC -- drawing to a bitmap
See CustomisedDrawing for more on using DC's in general.
This recipe also covers the basics of using a
wx.Bitmap
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
(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.
Question: Can anything plausibly get in the way of it being the right size, here?
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:
- Making a buffer the right size.
- Drawing to the buffer.
- Copying the buffer to the display.
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.
Question: How does the wx.BufferedDC different than the wx.BufferedPaintDC? Why was the one used in the one case, and the other in this case? What's so special about Painting, that it really requires another kind of drawing context?
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
BufferedCanvas -- a class that does what this does; it works on the same principles
RecipesImagesAndGraphics -- more drawing recipies
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

