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

[ATTACH]

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

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.

   1 class BufferedWindow(wx.Window):
   2 
   3     def __init__(self, *args, **kwargs):
   4         # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set.
   5         kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE
   6         wx.Window.__init__(self, *args, **kwargs)
   7 
   8         wx.EVT_PAINT(self, self.OnPaint)
   9         wx.EVT_SIZE(self, self.OnSize)
  10 
  11         # OnSize called to make sure the buffer is initialized.
  12         # This might result in OnSize getting called twice on some
  13         # platforms at initialization, but little harm done.
  14         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 init is done here, to make sure the buffer is always
   3         # the same size as the Window
   4         Size  = self.ClientSize
   5 
   6         # Make new offscreen bitmap: this bitmap will always have the
   7         # current drawing in it, so it can be used to save the image to
   8         # a file, or whatever.
   9         self._Buffer = wx.EmptyBitmap(*Size)
  10         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.

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.

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

   1     def UpdateDrawing(self):
   2         dc = wx.MemoryDC()
   3         dc.SelectObject(self._Buffer)
   4         self.Draw(dc)
   5         del dc # need to get rid of the MemoryDC before Update() is called.
   6         self.Refresh(eraseBackground=False)
   7         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.

   1     def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG):
   2         ## This will save the contents of the buffer
   3         ## to the specified file. See the wx 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, *args, **kwargs):
   3         ## Any data the Draw() function needs must be initialized before
   4         ## calling BufferedWindow.__init__, as it will call the Draw
   5         ## function.
   6         self.DrawData = {}
   7         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.

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

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

A simple wx.App to create the frame.

   1 class DemoApp(wx.App):
   2     def OnInit(self):
   3         frame = TestFrame()
   4         self.SetTopWindow(frame)
   5 
   6         return True
   7 
   8 if __name__ == "__main__":
   9     app = DemoApp(0)
  10     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 
   3 #!/usr/bin/env python
   4 
   5 import wx
   6 import random
   7 
   8 # This has been set up to optionally use the wx.BufferedDC if
   9 # USE_BUFFERED_DC is True, it will be used. Otherwise, it uses the raw
  10 # wx.Memory DC , etc.
  11 
  12 #USE_BUFFERED_DC = False
  13 USE_BUFFERED_DC = True
  14 
  15 class BufferedWindow(wx.Window):
  16 
  17     """
  18 
  19     A Buffered window class.
  20 
  21     To use it, subclass it and define a Draw(DC) method that takes a DC
  22     to draw to. In that method, put the code needed to draw the picture
  23     you want. The window will automatically be double buffered, and the
  24     screen will be automatically updated when a Paint event is received.
  25 
  26     When the drawing needs to change, you app needs to call the
  27     UpdateDrawing() method. Since the drawing is stored in a bitmap, you
  28     can also save the drawing to file by calling the
  29     SaveToFile(self, file_name, file_type) method.
  30 
  31     """
  32     def __init__(self, *args, **kwargs):
  33         # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set.
  34         kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE
  35         wx.Window.__init__(self, *args, **kwargs)
  36 
  37         wx.EVT_PAINT(self, self.OnPaint)
  38         wx.EVT_SIZE(self, self.OnSize)
  39 
  40         # OnSize called to make sure the buffer is initialized.
  41         # This might result in OnSize getting called twice on some
  42         # platforms at initialization, but little harm done.
  43         self.OnSize(None)
  44         self.paint_count = 0
  45 
  46     def Draw(self, dc):
  47         ## just here as a place holder.
  48         ## This method should be over-ridden when subclassed
  49         pass
  50 
  51     def OnPaint(self, event):
  52         # All that is needed here is to draw the buffer to screen
  53         if USE_BUFFERED_DC:
  54             dc = wx.BufferedPaintDC(self, self._Buffer)
  55         else:
  56             dc = wx.PaintDC(self)
  57             dc.DrawBitmap(self._Buffer, 0, 0)
  58 
  59     def OnSize(self,event):
  60         # The Buffer init is done here, to make sure the buffer is always
  61         # the same size as the Window
  62         #Size  = self.GetClientSizeTuple()
  63         Size  = self.ClientSize
  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=wx.BITMAP_TYPE_PNG):
  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         This code re-draws the buffer, then calls Update, which forces a paint event.
  86         """
  87         dc = wx.MemoryDC()
  88         dc.SelectObject(self._Buffer)
  89         self.Draw(dc)
  90         del dc # need to get rid of the MemoryDC before Update() is called.
  91         self.Refresh()
  92         self.Update()
  93             
  94 class DrawWindow(BufferedWindow):
  95     def __init__(self, *args, **kwargs):
  96         ## Any data the Draw() function needs must be initialized before
  97         ## calling BufferedWindow.__init__, as it will call the Draw
  98         ## function.
  99         self.DrawData = {}
 100         BufferedWindow.__init__(self, *args, **kwargs)
 101 
 102     def Draw(self, dc):
 103         dc.SetBackground( wx.Brush("White") )
 104         dc.Clear() # make sure you clear the bitmap!
 105 
 106         # Here's the actual drawing code.
 107         for key, data in self.DrawData.items():
 108             if key == "Rectangles":
 109                 dc.SetBrush(wx.BLUE_BRUSH)
 110                 dc.SetPen(wx.Pen('VIOLET', 4))
 111                 for r in data:
 112                     dc.DrawRectangle(*r)
 113             elif key == "Ellipses":
 114                 dc.SetBrush(wx.Brush("GREEN YELLOW"))
 115                 dc.SetPen(wx.Pen('CADET BLUE', 2))
 116                 for r in data:
 117                     dc.DrawEllipse(*r)
 118             elif key == "Polygons":
 119                 dc.SetBrush(wx.Brush("SALMON"))
 120                 dc.SetPen(wx.Pen('VIOLET RED', 4))
 121                 for r in data:
 122                     dc.DrawPolygon(r)
 123 
 124 
 125 class TestFrame(wx.Frame):
 126     def __init__(self, parent=None):
 127         wx.Frame.__init__(self, parent,
 128                           size = (500,500),
 129                           title="Double Buffered Test",
 130                           style=wx.DEFAULT_FRAME_STYLE)
 131 
 132         ## Set up the MenuBar
 133         MenuBar = wx.MenuBar()
 134         
 135         file_menu = wx.Menu()
 136         
 137         item = file_menu.Append(wx.ID_EXIT, text="&Exit")
 138         self.Bind(wx.EVT_MENU, self.OnQuit, item)
 139         MenuBar.Append(file_menu, "&File")
 140         
 141         draw_menu = wx.Menu()
 142         item = draw_menu.Append(wx.ID_ANY, "&New Drawing","Update the Drawing Data")
 143         self.Bind(wx.EVT_MENU, self.NewDrawing, item)
 144         item = draw_menu.Append(wx.ID_ANY,'&Save Drawing\tAlt-I','')
 145         self.Bind(wx.EVT_MENU, self.SaveToFile, item)
 146         MenuBar.Append(draw_menu, "&Draw")
 147 
 148         self.SetMenuBar(MenuBar)
 149         self.Window = DrawWindow(self)
 150         self.Show()
 151         # Initialize a drawing -- it has to be done after Show() is called
 152         #   so that the Windows has teh right size.
 153         self.NewDrawing()
 154 
 155     def OnQuit(self,event):
 156         self.Close(True)
 157         
 158     def NewDrawing(self, event=None):
 159         self.Window.DrawData = self.MakeNewData()
 160         self.Window.UpdateDrawing()
 161 
 162     def SaveToFile(self,event):
 163         dlg = wx.FileDialog(self, "Choose a file name to save the image as a PNG to",
 164                            defaultDir = "",
 165                            defaultFile = "",
 166                            wildcard = "*.png",
 167                            style = wx.SAVE)
 168         if dlg.ShowModal() == wx.ID_OK:
 169             self.Window.SaveToFile(dlg.GetPath(), wx.BITMAP_TYPE_PNG)
 170         dlg.Destroy()
 171 
 172     def MakeNewData(self):
 173         ## This method makes some random data to draw things with.
 174         MaxX, MaxY = self.Window.GetClientSizeTuple()
 175         DrawData = {}
 176 
 177         # make some random rectangles
 178         l = []
 179         for i in range(5):
 180             w = random.randint(1,MaxX/2)
 181             h = random.randint(1,MaxY/2)
 182             x = random.randint(1,MaxX-w)
 183             y = random.randint(1,MaxY-h)
 184             l.append( (x,y,w,h) )
 185         DrawData["Rectangles"] = l
 186 
 187         # make some random ellipses
 188         l = []
 189         for i in range(5):
 190             w = random.randint(1,MaxX/2)
 191             h = random.randint(1,MaxY/2)
 192             x = random.randint(1,MaxX-w)
 193             y = random.randint(1,MaxY-h)
 194             l.append( (x,y,w,h) )
 195         DrawData["Ellipses"] = l
 196 
 197         # Polygons
 198         l = []
 199         for i in range(3):
 200             points = []
 201             for j in range(random.randint(3,8)):
 202                 point = (random.randint(1,MaxX),random.randint(1,MaxY))
 203                 points.append(point)
 204             l.append(points)
 205         DrawData["Polygons"] = l
 206 
 207         return DrawData
 208 
 209 class DemoApp(wx.App):
 210     def OnInit(self):
 211         frame = TestFrame()
 212         self.SetTopWindow(frame)
 213 
 214         return True
 215 
 216 if __name__ == "__main__":
 217     app = DemoApp(0)
 218     app.MainLoop()

See Also

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:

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

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.

   1 class BufferedWindow(wx.Window):
   2 
   3     def __init__(self, *args, **kwargs):
   4         # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set.
   5         kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE
   6         wx.Window.__init__(self, *args, **kwargs)
   7 
   8         wx.EVT_PAINT(self, self.OnPaint)
   9         wx.EVT_SIZE(self, self.OnSize)
  10 
  11         # OnSize called to make sure the buffer is initialized.
  12         # This might result in OnSize getting called twice on some
  13         # platforms at initialization, but little harm done.
  14         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 init is done here, to make sure the buffer is always
   3         # the same size as the Window
   4         Size  = self.ClientSize
   5 
   6         # Make new offscreen bitmap: this bitmap will always have the
   7         # current drawing in it, so it can be used to save the image to
   8         # a file, or whatever.
   9         self._Buffer = wx.EmptyBitmap(*Size)
  10         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.

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.

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

   1     def UpdateDrawing(self):
   2         dc = wx.MemoryDC()
   3         dc.SelectObject(self._Buffer)
   4         self.Draw(dc)
   5         del dc # need to get rid of the MemoryDC before Update() is called.
   6         self.Refresh(eraseBackground=False)
   7         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.

   1     def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG):
   2         ## This will save the contents of the buffer
   3         ## to the specified file. See the wx 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, *args, **kwargs):
   3         ## Any data the Draw() function needs must be initialized before
   4         ## calling BufferedWindow.__init__, as it will call the Draw
   5         ## function.
   6         self.DrawData = {}
   7         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.

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

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

A simple wx.App to create the frame.

   1 class DemoApp(wx.App):
   2     def OnInit(self):
   3         frame = TestFrame()
   4         self.SetTopWindow(frame)
   5 
   6         return True
   7 
   8 if __name__ == "__main__":
   9     app = DemoApp(0)
  10     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 
   3 #!/usr/bin/env python
   4 
   5 import wx
   6 import random
   7 
   8 # This has been set up to optionally use the wx.BufferedDC if
   9 # USE_BUFFERED_DC is True, it will be used. Otherwise, it uses the raw
  10 # wx.Memory DC , etc.
  11 
  12 #USE_BUFFERED_DC = False
  13 USE_BUFFERED_DC = True
  14 
  15 class BufferedWindow(wx.Window):
  16 
  17     """
  18 
  19     A Buffered window class.
  20 
  21     To use it, subclass it and define a Draw(DC) method that takes a DC
  22     to draw to. In that method, put the code needed to draw the picture
  23     you want. The window will automatically be double buffered, and the
  24     screen will be automatically updated when a Paint event is received.
  25 
  26     When the drawing needs to change, you app needs to call the
  27     UpdateDrawing() method. Since the drawing is stored in a bitmap, you
  28     can also save the drawing to file by calling the
  29     SaveToFile(self, file_name, file_type) method.
  30 
  31     """
  32     def __init__(self, *args, **kwargs):
  33         # make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set.
  34         kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE
  35         wx.Window.__init__(self, *args, **kwargs)
  36 
  37         wx.EVT_PAINT(self, self.OnPaint)
  38         wx.EVT_SIZE(self, self.OnSize)
  39 
  40         # OnSize called to make sure the buffer is initialized.
  41         # This might result in OnSize getting called twice on some
  42         # platforms at initialization, but little harm done.
  43         self.OnSize(None)
  44         self.paint_count = 0
  45 
  46     def Draw(self, dc):
  47         ## just here as a place holder.
  48         ## This method should be over-ridden when subclassed
  49         pass
  50 
  51     def OnPaint(self, event):
  52         # All that is needed here is to draw the buffer to screen
  53         if USE_BUFFERED_DC:
  54             dc = wx.BufferedPaintDC(self, self._Buffer)
  55         else:
  56             dc = wx.PaintDC(self)
  57             dc.DrawBitmap(self._Buffer, 0, 0)
  58 
  59     def OnSize(self,event):
  60         # The Buffer init is done here, to make sure the buffer is always
  61         # the same size as the Window
  62         #Size  = self.GetClientSizeTuple()
  63         Size  = self.ClientSize
  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=wx.BITMAP_TYPE_PNG):
  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         This code re-draws the buffer, then calls Update, which forces a paint event.
  86         """
  87         dc = wx.MemoryDC()
  88         dc.SelectObject(self._Buffer)
  89         self.Draw(dc)
  90         del dc # need to get rid of the MemoryDC before Update() is called.
  91         self.Refresh()
  92         self.Update()
  93 
  94 class DrawWindow(BufferedWindow):
  95     def __init__(self, *args, **kwargs):
  96         ## Any data the Draw() function needs must be initialized before
  97         ## calling BufferedWindow.__init__, as it will call the Draw
  98         ## function.
  99         self.DrawData = {}
 100         BufferedWindow.__init__(self, *args, **kwargs)
 101 
 102     def Draw(self, dc):
 103         dc.SetBackground( wx.Brush("White") )
 104         dc.Clear() # make sure you clear the bitmap!
 105 
 106         # Here's the actual drawing code.
 107         for key, data in self.DrawData.items():
 108             if key == "Rectangles":
 109                 dc.SetBrush(wx.BLUE_BRUSH)
 110                 dc.SetPen(wx.Pen('VIOLET', 4))
 111                 for r in data:
 112                     dc.DrawRectangle(*r)
 113             elif key == "Ellipses":
 114                 dc.SetBrush(wx.Brush("GREEN YELLOW"))
 115                 dc.SetPen(wx.Pen('CADET BLUE', 2))
 116                 for r in data:
 117                     dc.DrawEllipse(*r)
 118             elif key == "Polygons":
 119                 dc.SetBrush(wx.Brush("SALMON"))
 120                 dc.SetPen(wx.Pen('VIOLET RED', 4))
 121                 for r in data:
 122                     dc.DrawPolygon(r)
 123 
 124 
 125 class TestFrame(wx.Frame):
 126     def __init__(self, parent=None):
 127         wx.Frame.__init__(self, parent,
 128                           size = (500,500),
 129                           title="Double Buffered Test",
 130                           style=wx.DEFAULT_FRAME_STYLE)
 131 
 132         ## Set up the MenuBar
 133         MenuBar = wx.MenuBar()
 134 
 135         file_menu = wx.Menu()
 136 
 137         item = file_menu.Append(wx.ID_EXIT, text="&Exit")
 138         self.Bind(wx.EVT_MENU, self.OnQuit, item)
 139         MenuBar.Append(file_menu, "&File")
 140 
 141         draw_menu = wx.Menu()
 142         item = draw_menu.Append(wx.ID_ANY, "&New Drawing","Update the Drawing Data")
 143         self.Bind(wx.EVT_MENU, self.NewDrawing, item)
 144         item = draw_menu.Append(wx.ID_ANY,'&Save Drawing\tAlt-I','')
 145         self.Bind(wx.EVT_MENU, self.SaveToFile, item)
 146         MenuBar.Append(draw_menu, "&Draw")
 147 
 148         self.SetMenuBar(MenuBar)
 149         self.Window = DrawWindow(self)
 150         self.Show()
 151         # Initialize a drawing -- it has to be done after Show() is called
 152         #   so that the Windows has teh right size.
 153         self.NewDrawing()
 154 
 155     def OnQuit(self,event):
 156         self.Close(True)
 157 
 158     def NewDrawing(self, event=None):
 159         self.Window.DrawData = self.MakeNewData()
 160         self.Window.UpdateDrawing()
 161 
 162     def SaveToFile(self,event):
 163         dlg = wx.FileDialog(self, "Choose a file name to save the image as a PNG to",
 164                            defaultDir = "",
 165                            defaultFile = "",
 166                            wildcard = "*.png",
 167                            style = wx.SAVE)
 168         if dlg.ShowModal() == wx.ID_OK:
 169             self.Window.SaveToFile(dlg.GetPath(), wx.BITMAP_TYPE_PNG)
 170         dlg.Destroy()
 171 
 172     def MakeNewData(self):
 173         ## This method makes some random data to draw things with.
 174         MaxX, MaxY = self.Window.GetClientSizeTuple()
 175         DrawData = {}
 176 
 177         # make some random rectangles
 178         l = []
 179         for i in range(5):
 180             w = random.randint(1,MaxX/2)
 181             h = random.randint(1,MaxY/2)
 182             x = random.randint(1,MaxX-w)
 183             y = random.randint(1,MaxY-h)
 184             l.append( (x,y,w,h) )
 185         DrawData["Rectangles"] = l
 186 
 187         # make some random ellipses
 188         l = []
 189         for i in range(5):
 190             w = random.randint(1,MaxX/2)
 191             h = random.randint(1,MaxY/2)
 192             x = random.randint(1,MaxX-w)
 193             y = random.randint(1,MaxY-h)
 194             l.append( (x,y,w,h) )
 195         DrawData["Ellipses"] = l
 196 
 197         # Polygons
 198         l = []
 199         for i in range(3):
 200             points = []
 201             for j in range(random.randint(3,8)):
 202                 point = (random.randint(1,MaxX),random.randint(1,MaxY))
 203                 points.append(point)
 204             l.append(points)
 205         DrawData["Polygons"] = l
 206 
 207         return DrawData
 208 
 209 class DemoApp(wx.App):
 210     def OnInit(self):
 211         frame = TestFrame()
 212         self.SetTopWindow(frame)
 213 
 214         return True
 215 
 216 if __name__ == "__main__":
 217     app = DemoApp(0)
 218     app.MainLoop()

See Also

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 Code Sample but revised to be compatible with Python 3.6 and the Phoenix implementation of wxPython. Try it, you might like it :-)

   1 # -*- coding: iso-8859-1 -*-#
   2 #!/usr/bin/env python
   3 # The following allows special characters to be in comments (e.g. the extended Swedish alphabet)
   4 # coding:utf-8
   5 """
   6  Purpose: Show how to draw with Phoenix (wxPython)
   7   Features:
   8    * Usage of wx.BufferedPaintDC() to reduce flicker    
   9    * Update a drawing
  10    * Override a method
  11    * Save a bitmap (drawing) to a file
  12    * Use super() for initialization of parent class
  13  Note:
  14   1. DC is an acronym for Device Context (where graphics and text can be drawn)
  15   2. Original code taken from: https://wiki.wxpython.org/DoubleBufferedDrawing 
  16      (for earlier versions of wxPython). Updated for Python 3 and Phoenix 4 by 
  17      Virsto (vs@it.uu.se)
  18 """
  19 import wx
  20 import random
  21 
  22 # This has been set up to use the wx.BufferedDC if
  23 # USE_BUFFERED_DC is True. Alternatively, it can use the raw
  24 # wx.MemoryDC, etc.
  25 
  26 #USE_BUFFERED_DC = False # use UpdateDrawing() method
  27 USE_BUFFERED_DC = True  # use Draw() method
  28 
  29 class BufferedWindow(wx.Window):
  30     '''
  31      A Buffered window class.
  32 
  33      To use it, subclass it and define a Draw() method that takes a DC
  34      to draw to. In that method, put the code needed to draw the picture
  35      you want. The window will automatically be double buffered, and the
  36      screen will be automatically updated when a Paint event is received
  37      (USE_BUFFERED_DC = True).
  38 
  39      When the drawing needs to change, your app needs to call the
  40      UpdateDrawing() method. Since the drawing is stored in a bitmap, you
  41      can also save the drawing to file by calling the SaveToFile() method.
  42     '''
  43     def __init__(self, *args, **kwargs):
  44         # Make sure the NO_FULL_REPAINT_ON_RESIZE style flag is set.
  45         # And define a new kwargs entry for wx.python
  46         kwargs['style'] = kwargs.setdefault('style', wx.NO_FULL_REPAINT_ON_RESIZE) | wx.NO_FULL_REPAINT_ON_RESIZE
  47         super().__init__( *args, **kwargs)
  48 
  49         # Setup event handlers for drawing 
  50         self.Bind(wx.EVT_PAINT,self.OnPaint)       
  51         self.Bind(wx.EVT_SIZE, self.OnSize)
  52 
  53         # OnSize called to make sure the buffer is initialized.
  54         # This might result in OnSize getting called twice on some
  55         # platforms at initialization, but little harm done.
  56         self.OnSize(None)
  57         self.paint_count = 0
  58 
  59     def Draw(self, dc):
  60         '''
  61          just here as a place holder.
  62          This method must be over-ridden when subclassed
  63         '''
  64         pass
  65 
  66     def OnPaint(self, event):
  67         '''
  68           All that is needed here is to move the buffer to the screen
  69         '''
  70         if USE_BUFFERED_DC:
  71             dc = wx.BufferedPaintDC(self, self._Buffer)
  72         else:
  73             dc = wx.PaintDC(self)
  74             dc.DrawBitmap(self._Buffer, 0, 0)
  75 
  76     def OnSize(self,event):
  77         '''
  78          The Buffer init is done here, to make sure the buffer is always
  79          the same size as the Window
  80         '''
  81         Size  = self.ClientSize
  82 
  83         # Make new offscreen bitmap: this bitmap will always have the
  84         # current drawing in it, so it can be used to save the image to
  85         # a file, or whatever.
  86         self._Buffer = wx.Bitmap(*Size)
  87         self.UpdateDrawing()
  88 
  89     def SaveToFile(self, FileName, FileType=wx.BITMAP_TYPE_PNG):
  90         '''
  91          This will save the contents of the buffer
  92          to the specified file. See the wx.Windows docs for 
  93          wx.Bitmap::SaveFile for the details
  94         '''
  95         self._Buffer.SaveFile(FileName, FileType)
  96 
  97     def UpdateDrawing(self):
  98         '''
  99          This would get called if the drawing is changed, for whatever reason.
 100 
 101          The idea here is that the drawing is based on some data generated
 102          elsewhere in the system. If that data changes, the drawing needs to
 103          be updated.
 104 
 105          This code re-draws the buffer, then calls Update, which forces a paint event.
 106         '''
 107         dc = wx.MemoryDC()
 108         dc.SelectObject(self._Buffer)
 109         self.Draw(dc)
 110         del dc      # need to get rid of the MemoryDC before Update() is called.
 111         self.Refresh()
 112         self.Update()
 113             
 114 class DrawWindow(BufferedWindow):
 115     '''
 116      Purpose: Initialization for Draw()
 117     '''
 118     def __init__(self, *args, **kwargs):
 119         '''
 120          Any data the Draw() function needs must be initialized before
 121          initialization of BufferedWindow, since this will trigger the Draw
 122          function.
 123         ''' 
 124         self.DrawData = {}
 125         super().__init__(*args, **kwargs)  # initialize parent (BufferedWindow)
 126 
 127     def Draw(self, dc):
 128         '''
 129          Purpose: to generate all the cmds for drawing 
 130           Note:
 131            1. Overrides Draw() in BufferedWindow() 
 132         '''
 133         dc.SetBackground( wx.Brush("White") )
 134         dc.Clear() # make sure you clear the bitmap!
 135 
 136         # Here's the actual drawing code.
 137         for key, data in self.DrawData.items():
 138             if key == "Rectangles":
 139                 dc.SetBrush(wx.BLUE_BRUSH)
 140                 dc.SetPen(wx.Pen('VIOLET', 4))
 141                 for r in data:
 142                     dc.DrawRectangle(*r)
 143             elif key == "Ellipses":
 144                 dc.SetBrush(wx.Brush("GREEN YELLOW"))
 145                 dc.SetPen(wx.Pen('CADET BLUE', 2))
 146                 for r in data:
 147                     dc.DrawEllipse(*r)
 148             elif key == "Polygons":
 149                 dc.SetBrush(wx.Brush("SALMON"))
 150                 dc.SetPen(wx.Pen('VIOLET RED', 4))
 151                 for r in data:
 152                     dc.DrawPolygon(r)
 153 
 154 class MyFrame(wx.Frame):
 155     '''
 156      Purpose: initialize a frame to hold DC and setup menu bar
 157     '''
 158     def __init__(self, parent=None):    
 159         wx.Frame.__init__(self, parent,
 160                           size  = (500,500),
 161                           title = "Double Buffered Test",
 162                           style = wx.DEFAULT_FRAME_STYLE)
 163 
 164         ## Set up the MenuBar
 165         MenuBar   = wx.MenuBar()
 166         
 167         file_menu = wx.Menu()
 168         
 169         # Define each item in menu bar and their event handlers
 170         item = file_menu.Append(wx.ID_EXIT, "&Exit")
 171         self.Bind(wx.EVT_MENU, self.OnQuit, item)
 172         MenuBar.Append(file_menu, "&File")
 173 
 174         draw_menu = wx.Menu()
 175         item = draw_menu.Append(wx.ID_ANY, "&New Drawing","Update the Drawing Data")
 176         self.Bind(wx.EVT_MENU, self.NewDrawing, item)
 177         item = draw_menu.Append(wx.ID_ANY,'&Save Drawing\tAlt-I','')
 178         self.Bind(wx.EVT_MENU, self.SaveToFile, item)
 179         MenuBar.Append(draw_menu, "&Draw")
 180 
 181         self.SetMenuBar(MenuBar)
 182         self.Window = DrawWindow(self) # initialize window for drawing
 183         self.Show()
 184         # Initialize a drawing -- it has to be done after Show() is called
 185         #   so that the Window is correctly sized.
 186         self.NewDrawing()
 187 
 188     def OnQuit(self,event):
 189         self.Close(True)
 190         
 191     def NewDrawing(self, event=None):
 192         '''
 193          Pupose: setup for updating drawing
 194         '''
 195         self.Window.DrawData = self.MakeNewData()
 196         self.Window.UpdateDrawing()
 197 
 198     def SaveToFile(self,event):
 199         '''
 200          Purpose: output image to a *.png file
 201         '''
 202         dlg = wx.FileDialog(self, "Choose a name for file to contain image",
 203                            defaultDir  = "",
 204                            defaultFile = "",
 205                            wildcard    = "*.png",
 206                            style       = wx.FD_SAVE)
 207         if dlg.ShowModal() == wx.ID_OK:
 208             self.Window.SaveToFile(dlg.GetPath(), wx.BITMAP_TYPE_PNG)
 209         dlg.Destroy()
 210 
 211     def MakeNewData(self):
 212         '''
 213          Purpose: generate some data for randomly shaped geometric 
 214                   structures (rectangles, ellipses and polygons)
 215         '''
 216         MaxX, MaxY = self.Window.GetClientSize() # to limit the size of these structures
 217         
 218         DrawData = {}
 219         # Define some random rectangles
 220         lst = []
 221         for i in range(5):
 222             w = random.randint(1,MaxX//2)
 223             h = random.randint(1,MaxY//2)
 224             x = random.randint(1,MaxX-w)
 225             y = random.randint(1,MaxY-h)
 226             lst.append( (x,y,w,h) )
 227         DrawData["Rectangles"] = lst
 228 
 229         # Define some random ellipses
 230         lst = []
 231         for i in range(5):
 232             w = random.randint(1,MaxX//2)
 233             h = random.randint(1,MaxY//2)
 234             x = random.randint(1,MaxX-w)
 235             y = random.randint(1,MaxY-h)
 236             lst.append( (x,y,w,h) )
 237         DrawData["Ellipses"] = lst
 238 
 239         # Define some random Polygons
 240         lst = []
 241         for i in range(3):
 242             points = []
 243             for j in range(random.randint(3,8)):
 244                 point = (random.randint(1,MaxX),random.randint(1,MaxY))
 245                 points.append(point)
 246             lst.append(points)
 247         DrawData["Polygons"] = lst
 248 
 249         return DrawData
 250 
 251 class DemoApp(wx.App):
 252     '''
 253      Purpose: initialize frame to hold DC
 254       Note:
 255        1. Uses the default OnInit method in wx.App
 256     '''
 257     def OnInit(self):
 258         frame = MyFrame()
 259         self.SetTopWindow(frame)
 260         return True
 261 
 262 if __name__ == "__main__":
 263     # Ok, let's run it ...
 264     app = DemoApp(0)
 265     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.

DoubleBufferedDrawing (last edited 2018-12-08 20:41:33 by Virgil Stokes)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.