== 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 DeviceContext''''''s (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 = {{drawing:wxBufferedWindowFlow}} (warning: ''drawing not yet updated to the wx namespace'') {{{ #!/usr/bin/env python import wx import random }}} Import the usual `wx` module. Import the `random` module, too; We'll need it for random data later in the demo. {{{ #USE_BUFFERED_DC = False USE_BUFFERED_DC = True }}} 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. {{{ #!python class BufferedWindow(wx.Window): def __init__(self, *args, **kwargs): # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set. kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE wx.Window.__init__(self, *args, **kwargs) wx.EVT_PAINT(self, self.OnPaint) wx.EVT_SIZE(self, self.OnSize) # OnSize called to make sure the buffer is initialized. # This might result in OnSize getting called twice on some # platforms at initialization, but little harm done. 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. {{{ #!python def Draw(self, dc): 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. {{{ #!python def OnSize(self,event): # The Buffer init is done here, to make sure the buffer is always # the same size as the Window Size = self.ClientSize # Make new offscreen bitmap: this bitmap will always have the # current drawing in it, so it can be used to save the image to # a file, or whatever. self._Buffer = wx.EmptyBitmap(*Size) 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`. `OnSize` does not trigger a call to `OnPaint()`. `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. {{{ #!python def OnPaint(self, event): # All that is needed here is to draw the buffer to screen if USE_BUFFERED_DC: dc = wx.BufferedPaintDC(self, self._Buffer) else: dc = wx.PaintDC(self) dc.DrawBitmap(self._Buffer, 0, 0) }}} Now the `OnPaint()` method. It's called whenever ther is a pain event sent by the system: i.e. whenever part of the window 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: the system has set the update region appropriately. 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; we've ''already'' drawn to the bitmap. So all we do is create the `wx.BufferedPaintDC`. Note that a `PaintDC` or `BufferedPaintDC` is unique -- it must be used only inside a paint event, and (on some systems, at least) a PaintDC ''must'' be created when there is a paint event. 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 essentially the same thing, but with an easier interface, for our purpose here. {{{ #!python def UpdateDrawing(self): dc = wx.MemoryDC() dc.SelectObject(self._Buffer) self.Draw(dc) del dc # need to get rid of the MemoryDC before Update() is called. self.Refresh(eraseBackground=False) self.Update() }}} `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 those 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. 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. After the drawing is done, the `MemoryDC` is destroyed, so that the `Bitmap` is freed to be used elsewhere (in the paint handler). The `Refresh(eraseBackground=False)`is called to tell the system that the Window needs to be redrawn (`eraseBackground=False` prevents the system from erasing the background before re-drawing -- which could cause flicker). `Update()` is called to force a paint event. The actual drawing to the screen happens in the paint event. {{{ #!python def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG): ## This will save the contents of the buffer ## to the specified file. See the wx docs for ## wx.Bitmap::SaveFile for the details 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. {{{ #!python class DrawWindow(BufferedWindow): def __init__(self, *args, **kwargs): ## Any data the Draw() function needs must be initialized before ## calling BufferedWindow.__init__, as it will call the Draw ## function. self.DrawData = {} BufferedWindow.__init__(self, *args, **kwargs) }}} 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. {{{ #!python def Draw(self, dc): dc.SetBackground( wx.Brush("White") ) dc.Clear() # make sure you clear the bitmap! # Here's the actual drawing code. for key, data in self.DrawData.items(): if key == "Rectangles": dc.SetBrush(wx.BLUE_BRUSH) dc.SetPen(wx.Pen('VIOLET', 4)) for r in data: dc.DrawRectangle(*r) elif key == "Ellipses": dc.SetBrush(wx.Brush("GREEN YELLOW")) dc.SetPen(wx.Pen('CADET BLUE', 2)) for r in data: dc.DrawEllipse(*r) elif key == "Polygons": dc.SetBrush(wx.Brush("SALMON")) dc.SetPen(wx.Pen('VIOLET RED', 4)) for r in data: dc.DrawPolygon(r) }}} = 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: {{{ #!python class TestFrame(wx.Frame): def __init__(self, parent=None): wx.Frame.__init__(self, parent, size = (500,500), title="Double Buffered Test", style=wx.DEFAULT_FRAME_STYLE) ## Set up the MenuBar MenuBar = wx.MenuBar() file_menu = wx.Menu() item = file_menu.Append(wx.ID_EXIT, text="&Exit") self.Bind(wx.EVT_MENU, self.OnQuit, item) MenuBar.Append(file_menu, "&File") draw_menu = wx.Menu() item = draw_menu.Append(wx.ID_ANY, "&New Drawing","Update the Drawing Data") self.Bind(wx.EVT_MENU, self.NewDrawing, item) item = draw_menu.Append(wx.ID_ANY,'&Save Drawing\tAlt-I','') self.Bind(wx.EVT_MENU, self.SaveToFile, item) MenuBar.Append(draw_menu, "&Draw") self.SetMenuBar(MenuBar) self.Window = DrawWindow(self) self.Show() # Initialize a drawing -- it has to be done after Show() is called # so that the Windows has teh right size. self.NewDrawing() def OnQuit(self,event): self.Close(True) def NewDrawing(self, event=None): self.Window.DrawData = self.MakeNewData() self.Window.UpdateDrawing() def SaveToFile(self,event): dlg = wx.FileDialog(self, "Choose a file name to save the image as a PNG to", defaultDir = "", defaultFile = "", wildcard = "*.png", style = wx.SAVE) if dlg.ShowModal() == wx.ID_OK: self.Window.SaveToFile(dlg.GetPath(), wx.BITMAP_TYPE_PNG) dlg.Destroy() def MakeNewData(self): ## This method makes some random data to draw things with. MaxX, MaxY = self.Window.GetClientSizeTuple() DrawData = {} # make some random rectangles l = [] for i in range(5): w = random.randint(1,MaxX/2) h = random.randint(1,MaxY/2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) l.append( (x,y,w,h) ) DrawData["Rectangles"] = l # make some random ellipses l = [] for i in range(5): w = random.randint(1,MaxX/2) h = random.randint(1,MaxY/2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) l.append( (x,y,w,h) ) DrawData["Ellipses"] = l # Polygons l = [] for i in range(3): points = [] for j in range(random.randint(3,8)): point = (random.randint(1,MaxX),random.randint(1,MaxY)) points.append(point) l.append(points) DrawData["Polygons"] = l return DrawData }}} A simple wx.App to create the frame. {{{ #!python class DemoApp(wx.App): def OnInit(self): frame = TestFrame() self.SetTopWindow(frame) return True if __name__ == "__main__": app = DemoApp(0) 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: {{{ #!python # -*- coding: iso-8859-1 -*-# #!/usr/bin/env python import wx import random # This has been set up to optionally use the wx.BufferedDC if # USE_BUFFERED_DC is True, it will be used. Otherwise, it uses the raw # wx.Memory DC , etc. #USE_BUFFERED_DC = False USE_BUFFERED_DC = True class BufferedWindow(wx.Window): """ A Buffered window class. To use it, subclass it and define a Draw(DC) method that takes a DC to draw to. In that method, put the code needed to draw the picture you want. The window will automatically be double buffered, and the screen will be automatically updated when a Paint event is received. When the drawing needs to change, you app needs to call the UpdateDrawing() method. Since the drawing is stored in a bitmap, you can also save the drawing to file by calling the SaveToFile(self, file_name, file_type) method. """ def __init__(self, *args, **kwargs): # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set. kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE wx.Window.__init__(self, *args, **kwargs) wx.EVT_PAINT(self, self.OnPaint) wx.EVT_SIZE(self, self.OnSize) # OnSize called to make sure the buffer is initialized. # This might result in OnSize getting called twice on some # platforms at initialization, but little harm done. self.OnSize(None) self.paint_count = 0 def Draw(self, dc): ## just here as a place holder. ## This method should be over-ridden when subclassed pass def OnPaint(self, event): # All that is needed here is to draw the buffer to screen if USE_BUFFERED_DC: dc = wx.BufferedPaintDC(self, self._Buffer) else: dc = wx.PaintDC(self) dc.DrawBitmap(self._Buffer, 0, 0) def OnSize(self,event): # The Buffer init is done here, to make sure the buffer is always # the same size as the Window #Size = self.GetClientSizeTuple() Size = self.ClientSize # Make new offscreen bitmap: this bitmap will always have the # current drawing in it, so it can be used to save the image to # a file, or whatever. self._Buffer = wx.EmptyBitmap(*Size) self.UpdateDrawing() def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG): ## This will save the contents of the buffer ## to the specified file. See the wxWindows docs for ## wx.Bitmap::SaveFile for the details self._Buffer.SaveFile(FileName, FileType) def UpdateDrawing(self): """ This would get called if the drawing needed to change, for whatever reason. The idea here is that the drawing is based on some data generated elsewhere in the system. If that data changes, the drawing needs to be updated. This code re-draws the buffer, then calls Update, which forces a paint event. """ dc = wx.MemoryDC() dc.SelectObject(self._Buffer) self.Draw(dc) del dc # need to get rid of the MemoryDC before Update() is called. self.Refresh() self.Update() class DrawWindow(BufferedWindow): def __init__(self, *args, **kwargs): ## Any data the Draw() function needs must be initialized before ## calling BufferedWindow.__init__, as it will call the Draw ## function. self.DrawData = {} BufferedWindow.__init__(self, *args, **kwargs) def Draw(self, dc): dc.SetBackground( wx.Brush("White") ) dc.Clear() # make sure you clear the bitmap! # Here's the actual drawing code. for key, data in self.DrawData.items(): if key == "Rectangles": dc.SetBrush(wx.BLUE_BRUSH) dc.SetPen(wx.Pen('VIOLET', 4)) for r in data: dc.DrawRectangle(*r) elif key == "Ellipses": dc.SetBrush(wx.Brush("GREEN YELLOW")) dc.SetPen(wx.Pen('CADET BLUE', 2)) for r in data: dc.DrawEllipse(*r) elif key == "Polygons": dc.SetBrush(wx.Brush("SALMON")) dc.SetPen(wx.Pen('VIOLET RED', 4)) for r in data: dc.DrawPolygon(r) class TestFrame(wx.Frame): def __init__(self, parent=None): wx.Frame.__init__(self, parent, size = (500,500), title="Double Buffered Test", style=wx.DEFAULT_FRAME_STYLE) ## Set up the MenuBar MenuBar = wx.MenuBar() file_menu = wx.Menu() item = file_menu.Append(wx.ID_EXIT, text="&Exit") self.Bind(wx.EVT_MENU, self.OnQuit, item) MenuBar.Append(file_menu, "&File") draw_menu = wx.Menu() item = draw_menu.Append(wx.ID_ANY, "&New Drawing","Update the Drawing Data") self.Bind(wx.EVT_MENU, self.NewDrawing, item) item = draw_menu.Append(wx.ID_ANY,'&Save Drawing\tAlt-I','') self.Bind(wx.EVT_MENU, self.SaveToFile, item) MenuBar.Append(draw_menu, "&Draw") self.SetMenuBar(MenuBar) self.Window = DrawWindow(self) self.Show() # Initialize a drawing -- it has to be done after Show() is called # so that the Windows has teh right size. self.NewDrawing() def OnQuit(self,event): self.Close(True) def NewDrawing(self, event=None): self.Window.DrawData = self.MakeNewData() self.Window.UpdateDrawing() def SaveToFile(self,event): dlg = wx.FileDialog(self, "Choose a file name to save the image as a PNG to", defaultDir = "", defaultFile = "", wildcard = "*.png", style = wx.SAVE) if dlg.ShowModal() == wx.ID_OK: self.Window.SaveToFile(dlg.GetPath(), wx.BITMAP_TYPE_PNG) dlg.Destroy() def MakeNewData(self): ## This method makes some random data to draw things with. MaxX, MaxY = self.Window.GetClientSizeTuple() DrawData = {} # make some random rectangles l = [] for i in range(5): w = random.randint(1,MaxX/2) h = random.randint(1,MaxY/2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) l.append( (x,y,w,h) ) DrawData["Rectangles"] = l # make some random ellipses l = [] for i in range(5): w = random.randint(1,MaxX/2) h = random.randint(1,MaxY/2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) l.append( (x,y,w,h) ) DrawData["Ellipses"] = l # Polygons l = [] for i in range(3): points = [] for j in range(random.randint(3,8)): point = (random.randint(1,MaxX),random.randint(1,MaxY)) points.append(point) l.append(points) DrawData["Polygons"] = l return DrawData class DemoApp(wx.App): def OnInit(self): frame = TestFrame() self.SetTopWindow(frame) return True if __name__ == "__main__": app = DemoApp(0) app.MainLoop() }}} == See Also == * BufferedCanvas -- a class that does what this does; it works on the same principles * RecipesImagesAndGraphics -- more drawing recipies === Comments === Updated for more "modern" method: - Chris Barker 3/15/11 Chris.Barker@noaa.gov == 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 = [[DoubleBufferedDrawing?action=AttachFile&do=get&target=wxBufferedWindowFlow.tdraw|attachimg]] (warning: ''drawing not yet updated to the wx namespace'') {{{#!/usr/bin/env python import wx import random }}} Import the usual `wx` module. Import the `random` module, too; We'll need it for random data later in the demo. {{{ #USE_BUFFERED_DC = False USE_BUFFERED_DC = True }}} 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. {{{#!python class BufferedWindow(wx.Window): def __init__(self, *args, **kwargs): # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set. kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE wx.Window.__init__(self, *args, **kwargs) wx.EVT_PAINT(self, self.OnPaint) wx.EVT_SIZE(self, self.OnSize) # OnSize called to make sure the buffer is initialized. # This might result in OnSize getting called twice on some # platforms at initialization, but little harm done. 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. {{{#!python def Draw(self, dc): 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. {{{#!python def OnSize(self,event): # The Buffer init is done here, to make sure the buffer is always # the same size as the Window Size = self.ClientSize # Make new offscreen bitmap: this bitmap will always have the # current drawing in it, so it can be used to save the image to # a file, or whatever. self._Buffer = wx.EmptyBitmap(*Size) 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`. `OnSize` does not trigger a call to `OnPaint()`. `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. {{{#!python def OnPaint(self, event): # All that is needed here is to draw the buffer to screen if USE_BUFFERED_DC: dc = wx.BufferedPaintDC(self, self._Buffer) else: dc = wx.PaintDC(self) dc.DrawBitmap(self._Buffer, 0, 0) }}} Now the `OnPaint()` method. It's called whenever ther is a pain event sent by the system: i.e. whenever part of the window 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: the system has set the update region appropriately. 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; we've ''already'' drawn to the bitmap. So all we do is create the `wx.BufferedPaintDC`. Note that a `PaintDC` or `BufferedPaintDC` is unique -- it must be used only inside a paint event, and (on some systems, at least) a PaintDC ''must'' be created when there is a paint event. 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 essentially the same thing, but with an easier interface, for our purpose here. {{{#!python def UpdateDrawing(self): dc = wx.MemoryDC() dc.SelectObject(self._Buffer) self.Draw(dc) del dc # need to get rid of the MemoryDC before Update() is called. self.Refresh(eraseBackground=False) self.Update() }}} `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 those 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. 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. After the drawing is done, the `MemoryDC` is destroyed, so that the `Bitmap` is freed to be used elsewhere (in the paint handler). The `Refresh(eraseBackground=False)`is called to tell the system that the Window needs to be redrawn (`eraseBackground=False` prevents the system from erasing the background before re-drawing -- which could cause flicker). `Update()` is called to force a paint event. The actual drawing to the screen happens in the paint event. {{{#!python def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG): ## This will save the contents of the buffer ## to the specified file. See the wx docs for ## wx.Bitmap::SaveFile for the details 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. {{{#!python class DrawWindow(BufferedWindow): def __init__(self, *args, **kwargs): ## Any data the Draw() function needs must be initialized before ## calling BufferedWindow.__init__, as it will call the Draw ## function. self.DrawData = {} BufferedWindow.__init__(self, *args, **kwargs) }}} 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. {{{#!python def Draw(self, dc): dc.SetBackground( wx.Brush("White") ) dc.Clear() # make sure you clear the bitmap! # Here's the actual drawing code. for key, data in self.DrawData.items(): if key == "Rectangles": dc.SetBrush(wx.BLUE_BRUSH) dc.SetPen(wx.Pen('VIOLET', 4)) for r in data: dc.DrawRectangle(*r) elif key == "Ellipses": dc.SetBrush(wx.Brush("GREEN YELLOW")) dc.SetPen(wx.Pen('CADET BLUE', 2)) for r in data: dc.DrawEllipse(*r) elif key == "Polygons": dc.SetBrush(wx.Brush("SALMON")) dc.SetPen(wx.Pen('VIOLET RED', 4)) for r in data: dc.DrawPolygon(r) }}} = 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: {{{#!python class TestFrame(wx.Frame): def __init__(self, parent=None): wx.Frame.__init__(self, parent, size = (500,500), title="Double Buffered Test", style=wx.DEFAULT_FRAME_STYLE) ## Set up the MenuBar MenuBar = wx.MenuBar() file_menu = wx.Menu() item = file_menu.Append(wx.ID_EXIT, text="&Exit") self.Bind(wx.EVT_MENU, self.OnQuit, item) MenuBar.Append(file_menu, "&File") draw_menu = wx.Menu() item = draw_menu.Append(wx.ID_ANY, "&New Drawing","Update the Drawing Data") self.Bind(wx.EVT_MENU, self.NewDrawing, item) item = draw_menu.Append(wx.ID_ANY,'&Save Drawing\tAlt-I','') self.Bind(wx.EVT_MENU, self.SaveToFile, item) MenuBar.Append(draw_menu, "&Draw") self.SetMenuBar(MenuBar) self.Window = DrawWindow(self) self.Show() # Initialize a drawing -- it has to be done after Show() is called # so that the Windows has teh right size. self.NewDrawing() def OnQuit(self,event): self.Close(True) def NewDrawing(self, event=None): self.Window.DrawData = self.MakeNewData() self.Window.UpdateDrawing() def SaveToFile(self,event): dlg = wx.FileDialog(self, "Choose a file name to save the image as a PNG to", defaultDir = "", defaultFile = "", wildcard = "*.png", style = wx.SAVE) if dlg.ShowModal() == wx.ID_OK: self.Window.SaveToFile(dlg.GetPath(), wx.BITMAP_TYPE_PNG) dlg.Destroy() def MakeNewData(self): ## This method makes some random data to draw things with. MaxX, MaxY = self.Window.GetClientSizeTuple() DrawData = {} # make some random rectangles l = [] for i in range(5): w = random.randint(1,MaxX/2) h = random.randint(1,MaxY/2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) l.append( (x,y,w,h) ) DrawData["Rectangles"] = l # make some random ellipses l = [] for i in range(5): w = random.randint(1,MaxX/2) h = random.randint(1,MaxY/2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) l.append( (x,y,w,h) ) DrawData["Ellipses"] = l # Polygons l = [] for i in range(3): points = [] for j in range(random.randint(3,8)): point = (random.randint(1,MaxX),random.randint(1,MaxY)) points.append(point) l.append(points) DrawData["Polygons"] = l return DrawData }}} A simple wx.App to create the frame. {{{#!python class DemoApp(wx.App): def OnInit(self): frame = TestFrame() self.SetTopWindow(frame) return True if __name__ == "__main__": app = DemoApp(0) 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: {{{#!python # -*- coding: iso-8859-1 -*-# #!/usr/bin/env python import wx import random # This has been set up to optionally use the wx.BufferedDC if # USE_BUFFERED_DC is True, it will be used. Otherwise, it uses the raw # wx.Memory DC , etc. #USE_BUFFERED_DC = False USE_BUFFERED_DC = True class BufferedWindow(wx.Window): """ A Buffered window class. To use it, subclass it and define a Draw(DC) method that takes a DC to draw to. In that method, put the code needed to draw the picture you want. The window will automatically be double buffered, and the screen will be automatically updated when a Paint event is received. When the drawing needs to change, you app needs to call the UpdateDrawing() method. Since the drawing is stored in a bitmap, you can also save the drawing to file by calling the SaveToFile(self, file_name, file_type) method. """ def __init__(self, *args, **kwargs): # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set. kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE wx.Window.__init__(self, *args, **kwargs) wx.EVT_PAINT(self, self.OnPaint) wx.EVT_SIZE(self, self.OnSize) # OnSize called to make sure the buffer is initialized. # This might result in OnSize getting called twice on some # platforms at initialization, but little harm done. self.OnSize(None) self.paint_count = 0 def Draw(self, dc): ## just here as a place holder. ## This method should be over-ridden when subclassed pass def OnPaint(self, event): # All that is needed here is to draw the buffer to screen if USE_BUFFERED_DC: dc = wx.BufferedPaintDC(self, self._Buffer) else: dc = wx.PaintDC(self) dc.DrawBitmap(self._Buffer, 0, 0) def OnSize(self,event): # The Buffer init is done here, to make sure the buffer is always # the same size as the Window #Size = self.GetClientSizeTuple() Size = self.ClientSize # Make new offscreen bitmap: this bitmap will always have the # current drawing in it, so it can be used to save the image to # a file, or whatever. self._Buffer = wx.EmptyBitmap(*Size) self.UpdateDrawing() def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG): ## This will save the contents of the buffer ## to the specified file. See the wxWindows docs for ## wx.Bitmap::SaveFile for the details self._Buffer.SaveFile(FileName, FileType) def UpdateDrawing(self): """ This would get called if the drawing needed to change, for whatever reason. The idea here is that the drawing is based on some data generated elsewhere in the system. If that data changes, the drawing needs to be updated. This code re-draws the buffer, then calls Update, which forces a paint event. """ dc = wx.MemoryDC() dc.SelectObject(self._Buffer) self.Draw(dc) del dc # need to get rid of the MemoryDC before Update() is called. self.Refresh() self.Update() class DrawWindow(BufferedWindow): def __init__(self, *args, **kwargs): ## Any data the Draw() function needs must be initialized before ## calling BufferedWindow.__init__, as it will call the Draw ## function. self.DrawData = {} BufferedWindow.__init__(self, *args, **kwargs) def Draw(self, dc): dc.SetBackground( wx.Brush("White") ) dc.Clear() # make sure you clear the bitmap! # Here's the actual drawing code. for key, data in self.DrawData.items(): if key == "Rectangles": dc.SetBrush(wx.BLUE_BRUSH) dc.SetPen(wx.Pen('VIOLET', 4)) for r in data: dc.DrawRectangle(*r) elif key == "Ellipses": dc.SetBrush(wx.Brush("GREEN YELLOW")) dc.SetPen(wx.Pen('CADET BLUE', 2)) for r in data: dc.DrawEllipse(*r) elif key == "Polygons": dc.SetBrush(wx.Brush("SALMON")) dc.SetPen(wx.Pen('VIOLET RED', 4)) for r in data: dc.DrawPolygon(r) class TestFrame(wx.Frame): def __init__(self, parent=None): wx.Frame.__init__(self, parent, size = (500,500), title="Double Buffered Test", style=wx.DEFAULT_FRAME_STYLE) ## Set up the MenuBar MenuBar = wx.MenuBar() file_menu = wx.Menu() item = file_menu.Append(wx.ID_EXIT, text="&Exit") self.Bind(wx.EVT_MENU, self.OnQuit, item) MenuBar.Append(file_menu, "&File") draw_menu = wx.Menu() item = draw_menu.Append(wx.ID_ANY, "&New Drawing","Update the Drawing Data") self.Bind(wx.EVT_MENU, self.NewDrawing, item) item = draw_menu.Append(wx.ID_ANY,'&Save Drawing\tAlt-I','') self.Bind(wx.EVT_MENU, self.SaveToFile, item) MenuBar.Append(draw_menu, "&Draw") self.SetMenuBar(MenuBar) self.Window = DrawWindow(self) self.Show() # Initialize a drawing -- it has to be done after Show() is called # so that the Windows has teh right size. self.NewDrawing() def OnQuit(self,event): self.Close(True) def NewDrawing(self, event=None): self.Window.DrawData = self.MakeNewData() self.Window.UpdateDrawing() def SaveToFile(self,event): dlg = wx.FileDialog(self, "Choose a file name to save the image as a PNG to", defaultDir = "", defaultFile = "", wildcard = "*.png", style = wx.SAVE) if dlg.ShowModal() == wx.ID_OK: self.Window.SaveToFile(dlg.GetPath(), wx.BITMAP_TYPE_PNG) dlg.Destroy() def MakeNewData(self): ## This method makes some random data to draw things with. MaxX, MaxY = self.Window.GetClientSizeTuple() DrawData = {} # make some random rectangles l = [] for i in range(5): w = random.randint(1,MaxX/2) h = random.randint(1,MaxY/2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) l.append( (x,y,w,h) ) DrawData["Rectangles"] = l # make some random ellipses l = [] for i in range(5): w = random.randint(1,MaxX/2) h = random.randint(1,MaxY/2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) l.append( (x,y,w,h) ) DrawData["Ellipses"] = l # Polygons l = [] for i in range(3): points = [] for j in range(random.randint(3,8)): point = (random.randint(1,MaxX),random.randint(1,MaxY)) points.append(point) l.append(points) DrawData["Polygons"] = l return DrawData class DemoApp(wx.App): def OnInit(self): frame = TestFrame() self.SetTopWindow(frame) return True if __name__ == "__main__": app = DemoApp(0) app.MainLoop() }}} == See Also == * BufferedCanvas -- a class that does what this does; it works on the same principles * RecipesImagesAndGraphics -- more drawing recipies === Comments === Updated for more "modern" method: - Chris Barker 3/15/11 Chris.Barker@noaa.gov = Code for a new "newly defined buffered Window" = The code is taken from [[#CB_Code|Code Sample]] but revised to be compatible with Python 3.6 and the Phoenix implementation of wxPython. Try it, you might like it :-) {{{ #!python # -*- coding: iso-8859-1 -*-# #!/usr/bin/env python # The following allows special characters to be in comments (e.g. the extended Swedish alphabet) # coding:utf-8 """ Purpose: Show how to draw with Phoenix (wxPython) Features: * Usage of wx.BufferedPaintDC() to reduce flicker * Update a drawing * Override a method * Save a bitmap (drawing) to a file * Use super() for initialization of parent class Note: 1. DC is an acronym for Device Context (where graphics and text can be drawn) 2. Original code taken from: https://wiki.wxpython.org/DoubleBufferedDrawing (for earlier versions of wxPython). Updated for Python 3 and Phoenix 4 by Virsto (vs@it.uu.se) """ import wx import random # This has been set up to use the wx.BufferedDC if # USE_BUFFERED_DC is True. Alternatively, it can use the raw # wx.MemoryDC, etc. #USE_BUFFERED_DC = False # use UpdateDrawing() method USE_BUFFERED_DC = True # use Draw() method class BufferedWindow(wx.Window): ''' A Buffered window class. To use it, subclass it and define a Draw() method that takes a DC to draw to. In that method, put the code needed to draw the picture you want. The window will automatically be double buffered, and the screen will be automatically updated when a Paint event is received (USE_BUFFERED_DC = True). When the drawing needs to change, your app needs to call the UpdateDrawing() method. Since the drawing is stored in a bitmap, you can also save the drawing to file by calling the SaveToFile() method. ''' def __init__(self, *args, **kwargs): # Make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set. # And define a new kwargs entry for wx.python kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE super().__init__( *args, **kwargs) # Setup event handlers for drawing self.Bind(wx.EVT_PAINT,self.OnPaint) self.Bind(wx.EVT_SIZE, self.OnSize) # OnSize called to make sure the buffer is initialized. # This might result in OnSize getting called twice on some # platforms at initialization, but little harm done. self.OnSize(None) self.paint_count = 0 def Draw(self, dc): ''' just here as a place holder. This method must be over-ridden when subclassed ''' pass def OnPaint(self, event): ''' All that is needed here is to move the buffer to the screen ''' if USE_BUFFERED_DC: dc = wx.BufferedPaintDC(self, self._Buffer) else: dc = wx.PaintDC(self) dc.DrawBitmap(self._Buffer, 0, 0) def OnSize(self,event): ''' The Buffer init is done here, to make sure the buffer is always the same size as the Window ''' Size = self.ClientSize # Make new offscreen bitmap: this bitmap will always have the # current drawing in it, so it can be used to save the image to # a file, or whatever. self._Buffer = wx.Bitmap(*Size) self.UpdateDrawing() def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG): ''' This will save the contents of the buffer to the specified file. See the wx.Windows docs for wx.Bitmap::SaveFile for the details ''' self._Buffer.SaveFile(FileName, FileType) def UpdateDrawing(self): ''' This would get called if the drawing is changed, for whatever reason. The idea here is that the drawing is based on some data generated elsewhere in the system. If that data changes, the drawing needs to be updated. This code re-draws the buffer, then calls Update, which forces a paint event. ''' dc = wx.MemoryDC() dc.SelectObject(self._Buffer) self.Draw(dc) del dc # need to get rid of the MemoryDC before Update() is called. self.Refresh() self.Update() class DrawWindow(BufferedWindow): ''' Purpose: Initialization for Draw() ''' def __init__(self, *args, **kwargs): ''' Any data the Draw() function needs must be initialized before initialization of BufferedWindow, since this will trigger the Draw function. ''' self.DrawData = {} super().__init__(*args, **kwargs) # initialize parent (BufferedWindow) def Draw(self, dc): ''' Purpose: to generate all the cmds for drawing Note: 1. Overrides Draw() in BufferedWindow() ''' dc.SetBackground( wx.Brush("White") ) dc.Clear() # make sure you clear the bitmap! # Here's the actual drawing code. for key, data in self.DrawData.items(): if key == "Rectangles": dc.SetBrush(wx.BLUE_BRUSH) dc.SetPen(wx.Pen('VIOLET', 4)) for r in data: dc.DrawRectangle(*r) elif key == "Ellipses": dc.SetBrush(wx.Brush("GREEN YELLOW")) dc.SetPen(wx.Pen('CADET BLUE', 2)) for r in data: dc.DrawEllipse(*r) elif key == "Polygons": dc.SetBrush(wx.Brush("SALMON")) dc.SetPen(wx.Pen('VIOLET RED', 4)) for r in data: dc.DrawPolygon(r) class MyFrame(wx.Frame): ''' Purpose: initialize a frame to hold DC and setup menu bar ''' def __init__(self, parent=None): wx.Frame.__init__(self, parent, size = (500,500), title = "Double Buffered Test", style = wx.DEFAULT_FRAME_STYLE) ## Set up the MenuBar MenuBar = wx.MenuBar() file_menu = wx.Menu() # Define each item in menu bar and their event handlers item = file_menu.Append(wx.ID_EXIT, "&Exit") self.Bind(wx.EVT_MENU, self.OnQuit, item) MenuBar.Append(file_menu, "&File") draw_menu = wx.Menu() item = draw_menu.Append(wx.ID_ANY, "&New Drawing","Update the Drawing Data") self.Bind(wx.EVT_MENU, self.NewDrawing, item) item = draw_menu.Append(wx.ID_ANY,'&Save Drawing\tAlt-I','') self.Bind(wx.EVT_MENU, self.SaveToFile, item) MenuBar.Append(draw_menu, "&Draw") self.SetMenuBar(MenuBar) self.Window = DrawWindow(self) # initialize window for drawing self.Show() # Initialize a drawing -- it has to be done after Show() is called # so that the Window is correctly sized. self.NewDrawing() def OnQuit(self,event): self.Close(True) def NewDrawing(self, event=None): ''' Pupose: setup for updating drawing ''' self.Window.DrawData = self.MakeNewData() self.Window.UpdateDrawing() def SaveToFile(self,event): ''' Purpose: output image to a *.png file ''' dlg = wx.FileDialog(self, "Choose a name for file to contain image", defaultDir = "", defaultFile = "", wildcard = "*.png", style = wx.FD_SAVE) if dlg.ShowModal() == wx.ID_OK: self.Window.SaveToFile(dlg.GetPath(), wx.BITMAP_TYPE_PNG) dlg.Destroy() def MakeNewData(self): ''' Purpose: generate some data for randomly shaped geometric structures (rectangles, ellipses and polygons) ''' MaxX, MaxY = self.Window.GetClientSize() # to limit the size of these structures DrawData = {} # Define some random rectangles lst = [] for i in range(5): w = random.randint(1,MaxX//2) h = random.randint(1,MaxY//2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) lst.append( (x,y,w,h) ) DrawData["Rectangles"] = lst # Define some random ellipses lst = [] for i in range(5): w = random.randint(1,MaxX//2) h = random.randint(1,MaxY//2) x = random.randint(1,MaxX-w) y = random.randint(1,MaxY-h) lst.append( (x,y,w,h) ) DrawData["Ellipses"] = lst # Define some random Polygons lst = [] for i in range(3): points = [] for j in range(random.randint(3,8)): point = (random.randint(1,MaxX),random.randint(1,MaxY)) points.append(point) lst.append(points) DrawData["Polygons"] = lst return DrawData class DemoApp(wx.App): ''' Purpose: initialize frame to hold DC Note: 1. Uses the default OnInit method in wx.App ''' def OnInit(self): frame = MyFrame() self.SetTopWindow(frame) return True if __name__ == "__main__": # Ok, let's run it ... app = DemoApp(0) app.MainLoop() }}} === Comments === Special thanks to Robin Dunn (creator of wxPython) and Chris Barker (creator of this page and developer of wxPython applications). I hope that this minor revision will encourage others to develop more advanced Phoenix apps - Virgil Stokes 2018/12/08 vs@it.uu.se.