How to create an animated drawing (Phoenix)
Keywords : Drawing, Animation, Motion, Bubbles, Tetris, BufferedPaintDC, GCDC, KeyCode.
Contents
Demonstrating :
Tested py3.x, wx4.x and Win10.
Are you ready to use some samples ?
Test, modify, correct, complete, improve and share your discoveries !
Bubbles toy
Here's a fun little toy program.
It's a little piece of interactive art.
When you run it:
- Try moving your mouse over the panel, at different speeds.
Try the left & right mouse buttons.
Press upper case & lower case letters, repeatedly, over the display area, as you move the mouse over it.
I thought it was cute, at any rate !
If you add anything to the program, (pretty easy to do!,) please, by all means, put your additions here !
Note that this code demonstrates DoubleBufferedDrawing on platforms where such is needed for flicker-free animation.
1 # bubbles_toy.py
2
3 import wx
4 import math
5 import random
6
7 # class MyBubble
8 # class MyBubblePanel
9 # class MyBubbleFrame
10
11 #---------------------------------------------------------------------------
12
13 class MyBubble(object):
14 def __init__(self, x, y, death_size=25, color="GREEN", grow_speed=0.1):
15
16 self.x = x
17 self.y = y
18 self.radius = 5
19 self.death_size = death_size
20 self.color = color
21 self.grow_speed = grow_speed
22
23 #-----------------------------------------------------------------------
24
25 def go(self, bubbles):
26 growth = math.log(self.radius) * self.grow_speed
27 self.radius = self.radius + growth
28 if self.radius >= self.death_size:
29 bubbles.remove(self)
30
31
32 def draw(self, dc, bubbles):
33 dc.SetPen(wx.Pen(self.color, 2))
34 dc.DrawCircle(int(self.x), int(self.y), int(self.radius))
35 self.go(bubbles)
36
37 #---------------------------------------------------------------------------
38
39 class MyBubblePanel(wx.Window):
40 def __init__(self, parent):
41 wx.Window.__init__(self, parent)
42
43 self.bubbles = []
44 self.marks = []
45 self.last_pos = self.ScreenToClient(wx.GetMousePosition())
46 self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)
47 self.SetBackgroundColour("WHITE")
48
49 self.Bind(wx.EVT_PAINT, self.on_paint)
50 self.Bind(wx.EVT_SIZE, self.on_size)
51
52 self.Bind(wx.EVT_MOTION, self.on_motion)
53 self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
54 self.Bind(wx.EVT_RIGHT_UP, self.on_right_up)
55 self.Bind(wx.EVT_CHAR, self.on_character)
56
57 wx.CallLater(200, self.SetFocus)
58
59 #-----------------------------------------------------------------------
60
61 def on_size(self, event):
62 width, height = self.GetClientSize()
63 self._buffer = wx.Bitmap(width, height)
64 self.update_drawing()
65
66
67 def update_drawing(self):
68 self.Refresh(False)
69
70
71 def on_paint(self, event):
72 # dc = wx.AutoBufferedPaintDC(self)
73 # or
74 dc = wx.BufferedPaintDC(self)
75 dc = wx.GCDC(dc)
76 dc.Clear()
77
78 x, y = self.ScreenToClient(wx.GetMousePosition())
79
80 self.draw_x(dc, x, y, 4)
81 for bubble in self.bubbles:
82 bubble.draw(dc, self.bubbles)
83 self.draw_marks(dc)
84
85
86 def draw_x(self, dc, x, y, line_width):
87 dc.SetPen(wx.Pen("BLACK", line_width))
88 dc.DrawLine(x-5, y-5, x+5, y+5) # \
89 dc.DrawLine(x-5, y+5, x+5, y-5) # /
90
91
92 def draw_marks(self, dc):
93 chains = {}
94 for (letter, x, y) in self.marks:
95 self.draw_x(dc, x, y, 2)
96 dc.DrawText(letter, x-3, y-28)
97 chains.setdefault(letter, []).append(wx.Point(x,y))
98 for (key, points) in chains.items():
99 if len(points) > 1:
100 if key == key.upper() or len(points) == 2:
101 dc.DrawLines(points)
102 else:
103 dc.DrawSpline(points)
104
105
106 def on_motion(self, event):
107 x, y = event.GetPosition()
108 motion_score = (abs(x - self.last_pos.x) + abs(y - self.last_pos.y))
109 self.last_pos = wx.Point(x, y)
110 if random.randint(0, motion_score) > 5:
111 self.bubbles.append(MyBubble(x, y))
112 if random.randint(0, 100) == 0:
113 self.bubbles.append(
114 MyBubble(x, y, color="PURPLE", death_size=100, grow_speed=0.5))
115
116
117 def on_left_up(self, event):
118 self.bubbles.append(MyBubble(event.GetX(), event.GetY(),
119 color="YELLOW", death_size=50, grow_speed=0.1))
120
121
122 def on_right_up(self, event):
123 self.bubbles.append(MyBubble(event.GetX(), event.GetY(),
124 color="BLUE", death_size=80, grow_speed=0.6))
125
126
127 def on_character(self, event):
128 key = event.GetKeyCode()
129 if key==27: # Esc key
130 self.marks = []
131 else:
132 x, y = self.ScreenToClient(wx.GetMousePosition())
133 self.marks.append( (chr(event.GetKeyCode()), x, y) )
134
135 #---------------------------------------------------------------------------
136
137 class MyBubbleFrame(wx.Frame):
138 def __init__(self, *args, **kw):
139 wx.Frame.__init__(self, *args, **kw)
140
141 self.SetIcon(wx.Icon('wxwin.ico'))
142
143 self.Bind(wx.EVT_CLOSE, self.on_close)
144 self.Bind(wx.EVT_TIMER, self.on_timer)
145
146 self.panel = MyBubblePanel(self)
147 self.timer = wx.Timer(self)
148 self.timer.Start(20)
149
150 #-----------------------------------------------------------------------
151
152 def on_close(self, event):
153 self.timer.Stop()
154 self.Destroy()
155
156
157 def on_timer(self, event):
158 self.panel.update_drawing()
159
160 #---------------------------------------------------------------------------
161
162 app = wx.App(False)
163 frame = MyBubbleFrame(None, -1, "Bubbles toy !")
164 frame.Show(True)
165 app.MainLoop()
Tetris
1 # tetris.py
2
3 """
4 ZetCode wxPython tutorial.
5
6 This is Tetris game clone in wxPython.
7
8 Author : Jan Bodnar
9 Website : www.zetcode.com
10 Link : http://zetcode.com/wxpython/thetetrisgame/
11 Last modified : May 2018
12 """
13
14 import wx
15 import random
16
17 # class Tetris
18 # class Board
19 # class Tetrominoes
20 # class Shape
21
22 #---------------------------------------------------------------------------
23
24 class Tetris(wx.Frame):
25 """
26 ...
27 """
28 def __init__(self, parent):
29 wx.Frame.__init__(self, parent, size=(220, 420),
30 style=wx.DEFAULT_FRAME_STYLE ^ wx.RESIZE_BORDER ^ wx.MAXIMIZE_BOX)
31
32 self.initFrame()
33
34 #-----------------------------------------------------------------------
35
36 def initFrame(self):
37 """
38 ...
39 """
40
41 self.statusbar = self.CreateStatusBar()
42 self.statusbar.SetStatusText('0')
43 self.board = Board(self)
44 self.board.SetFocus()
45 self.board.start()
46
47 #------------
48
49 # Creating the menubar.
50 menu_bar = wx.MenuBar()
51
52 # Setting up the menu.
53 file_menu = wx.Menu()
54
55 # wx.ID_ABOUT and wx.ID_EXIT are standard IDs provided by wxWidgets.
56 item = file_menu.Append(wx.ID_ABOUT, "&About", "Information about Tetris.")
57 self.Bind(wx.EVT_MENU, self.OnAbout, item)
58 file_menu.AppendSeparator()
59 item = file_menu.Append(wx.ID_EXIT, "E&xit", "Exit Tetris.")
60 self.Bind(wx.EVT_MENU, self.OnQuit, item)
61
62 # Create the "Help" top menu.
63 help_menu = wx.Menu()
64 item = help_menu.Append(wx.ID_HELP, "&Shortcuts", "Shortcuts for Tetris.")
65 self.Bind(wx.EVT_MENU, self.OnHelp, item)
66
67 # Adding the "file_menu" to the menu bar.
68 menu_bar.Append(file_menu, "&File")
69 menu_bar.Append(help_menu, "&Help")
70
71 # Adding the menu bar to the frame content.
72 self.SetMenuBar(menu_bar)
73
74 #------------
75
76 self.SetIcon(wx.Icon("wxwin.ico"))
77 self.SetTitle("Tetris")
78 self.CenterOnScreen(wx.BOTH)
79
80
81 def OnQuit(self, event):
82 self.Destroy()
83
84
85 def OnAbout(self, event):
86 dlg = wx.MessageDialog(self,
87 "This is a small game.\n",
88 "About Tetris",
89 wx.OK | wx.CENTRE | wx.ICON_INFORMATION)
90 dlg.ShowModal()
91 dlg.Destroy()
92
93
94 def OnHelp(self, event):
95 dlg = wx.MessageDialog(self,
96 "AZERTY keyboard :\n"
97 "--------------------\n"
98 "P \t---------> \tPause\n"
99 "S \t---------> \tLEFT\n"
100 "F \t---------> \tRIGHT\n"
101 "D \t---------> \tDOWN (rotated right)\n"
102 "E \t----------> \tUP (rotated left)\n"
103 "SPACE \t----> \tAccelerate\n"
104 "D \t---------> \tOne line down\n",
105 "Shortcuts",
106 wx.OK | wx.CENTRE | wx.ICON_INFORMATION)
107 dlg.ShowModal()
108 dlg.Destroy()
109
110 #---------------------------------------------------------------------------
111
112 class Board(wx.Panel):
113 """
114 ...
115 """
116
117 BoardWidth = 10
118 BoardHeight = 22
119 Speed = 300
120 ID_TIMER = 1
121
122 def __init__(self, *args, **kw):
123 super(Board, self).__init__(*args, **kw)
124
125 self.initBoard()
126
127 #------------
128
129 # Delete flickers.
130 if wx.Platform == "__WXMSW__":
131 self.SetDoubleBuffered(True)
132
133 #-----------------------------------------------------------------------
134
135 def initBoard(self):
136 """
137 ...
138 """
139
140 self.timer = wx.Timer(self, Board.ID_TIMER)
141 self.isWaitingAfterLine = False
142 self.curPiece = Shape()
143 self.nextPiece = Shape()
144 self.curX = 0
145 self.curY = 0
146 self.numLinesRemoved = 0
147 self.board = []
148
149 self.isStarted = False
150 self.isPaused = False
151
152 self.Bind(wx.EVT_PAINT, self.OnPaint)
153 self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
154 self.Bind(wx.EVT_TIMER, self.OnTimer, id=Board.ID_TIMER)
155
156 self.clearBoard()
157
158
159 def shapeAt(self, x, y):
160 """
161 ...
162 """
163
164 return self.board[(y * Board.BoardWidth) + x]
165
166
167 def setShapeAt(self, x, y, shape):
168 """
169 ...
170 """
171
172 self.board[(y * Board.BoardWidth) + x] = shape
173
174
175 def squareWidth(self):
176 """
177 ...
178 """
179
180 return self.GetClientSize().GetWidth() // Board.BoardWidth
181
182
183 def squareHeight(self):
184 """
185 ...
186 """
187
188 return self.GetClientSize().GetHeight() // Board.BoardHeight
189
190
191 def start(self):
192 """
193 ...
194 """
195
196 if self.isPaused:
197 return
198
199 self.isStarted = True
200 self.isWaitingAfterLine = False
201 self.numLinesRemoved = 0
202 self.clearBoard()
203
204 self.newPiece()
205 self.timer.Start(Board.Speed)
206
207
208 def pause(self):
209 """
210 ...
211 """
212
213 if not self.isStarted:
214 return
215
216 self.isPaused = not self.isPaused
217 statusbar = self.GetParent().statusbar
218
219 if self.isPaused:
220 self.timer.Stop()
221 statusbar.SetStatusText('paused')
222 else:
223 self.timer.Start(Board.Speed)
224 statusbar.SetStatusText(str(self.numLinesRemoved))
225
226 self.Refresh()
227
228
229 def clearBoard(self):
230 """
231 ...
232 """
233
234 for i in range(Board.BoardHeight * Board.BoardWidth):
235 self.board.append(Tetrominoes.NoShape)
236
237
238 def OnPaint(self, event):
239 """
240 ...
241 """
242
243 dc = wx.PaintDC(self)
244
245 size = self.GetClientSize()
246 boardTop = size.GetHeight() - Board.BoardHeight * self.squareHeight()
247
248 for i in range(Board.BoardHeight):
249 for j in range(Board.BoardWidth):
250
251 shape = self.shapeAt(j, Board.BoardHeight - i - 1)
252
253 if shape != Tetrominoes.NoShape:
254 self.drawSquare(dc,
255 0 + j * self.squareWidth(),
256 boardTop + i * self.squareHeight(), shape)
257
258 if self.curPiece.shape() != Tetrominoes.NoShape:
259
260 for i in range(4):
261
262 x = self.curX + self.curPiece.x(i)
263 y = self.curY - self.curPiece.y(i)
264
265 self.drawSquare(dc, 0 + x * self.squareWidth(),
266 boardTop + (Board.BoardHeight - y - 1) * self.squareHeight(),
267 self.curPiece.shape())
268
269
270 def OnKeyDown(self, event):
271 """
272 ...
273 """
274
275 if not self.isStarted or self.curPiece.shape() == Tetrominoes.NoShape:
276 event.Skip()
277 return
278
279 keycode = event.GetKeyCode()
280 print("Keycode :", keycode)
281
282 if keycode == ord('P') or keycode == ord('p'):
283 self.pause()
284 return
285
286 if self.isPaused:
287 return
288
289 #-----------------
290 # AZERTY keyboard.
291 #-----------------
292 # elif keycode == wx.WXK_LEFT:
293 elif keycode == ord('S') or keycode == ord('s'):
294 self.tryMove(self.curPiece, self.curX - 1, self.curY)
295
296 # elif keycode == wx.WXK_RIGHT:
297 elif keycode == ord('F') or keycode == ord('f'):
298 self.tryMove(self.curPiece, self.curX + 1, self.curY)
299
300 # elif keycode == wx.WXK_DOWN:
301 elif keycode == ord('D') or keycode == ord('d'):
302 self.tryMove(self.curPiece.rotatedRight(), self.curX, self.curY)
303
304 # elif keycode == wx.WXK_UP:
305 elif keycode == ord('E') or keycode == ord('e'):
306 self.tryMove(self.curPiece.rotatedLeft(), self.curX, self.curY)
307
308 elif keycode == wx.WXK_SPACE:
309 self.dropDown()
310
311 elif keycode == ord('R') or keycode == ord('r'):
312 self.oneLineDown()
313
314 else:
315 event.Skip()
316
317
318 def OnTimer(self, event):
319 """
320 ...
321 """
322
323 if event.GetId() == Board.ID_TIMER:
324
325 if self.isWaitingAfterLine:
326 self.isWaitingAfterLine = False
327 self.newPiece()
328
329 else:
330 self.oneLineDown()
331
332 else:
333 event.Skip()
334
335
336 def dropDown(self):
337 """
338 ...
339 """
340
341 newY = self.curY
342
343 while newY > 0:
344 if not self.tryMove(self.curPiece, self.curX, newY - 1):
345 break
346 newY -= 1
347
348 self.pieceDropped()
349
350
351 def oneLineDown(self):
352 """
353 ...
354 """
355
356 if not self.tryMove(self.curPiece, self.curX, self.curY - 1):
357 self.pieceDropped()
358
359
360 def pieceDropped(self):
361 """
362 ...
363 """
364
365 for i in range(4):
366
367 x = self.curX + self.curPiece.x(i)
368 y = self.curY - self.curPiece.y(i)
369 self.setShapeAt(x, y, self.curPiece.shape())
370
371 self.removeFullLines()
372
373 if not self.isWaitingAfterLine:
374 self.newPiece()
375
376
377 def removeFullLines(self):
378 """
379 ...
380 """
381
382 numFullLines = 0
383
384 statusbar = self.GetParent().statusbar
385
386 rowsToRemove = []
387
388 for i in range(Board.BoardHeight):
389 n = 0
390 for j in range(Board.BoardWidth):
391 if not self.shapeAt(j, i) == Tetrominoes.NoShape:
392 n = n + 1
393
394 if n == 10:
395 rowsToRemove.append(i)
396
397 rowsToRemove.reverse()
398
399 for m in rowsToRemove:
400 for k in range(m, Board.BoardHeight):
401 for l in range(Board.BoardWidth):
402 self.setShapeAt(l, k, self.shapeAt(l, k + 1))
403
404 numFullLines = numFullLines + len(rowsToRemove)
405
406 if numFullLines > 0:
407
408 self.numLinesRemoved = self.numLinesRemoved + numFullLines
409 statusbar.SetStatusText(str(self.numLinesRemoved))
410 self.isWaitingAfterLine = True
411 self.curPiece.setShape(Tetrominoes.NoShape)
412 self.Refresh()
413
414
415 def newPiece(self):
416 """
417 ...
418 """
419
420 self.curPiece = self.nextPiece
421 statusbar = self.GetParent().statusbar
422 self.nextPiece.setRandomShape()
423
424 self.curX = Board.BoardWidth // 2 + 1
425 self.curY = Board.BoardHeight - 1 + self.curPiece.minY()
426
427 if not self.tryMove(self.curPiece, self.curX, self.curY):
428
429 self.curPiece.setShape(Tetrominoes.NoShape)
430 self.timer.Stop()
431 self.isStarted = False
432 statusbar.SetStatusText('Game over')
433
434
435 def tryMove(self, newPiece, newX, newY):
436 """
437 ...
438 """
439
440 for i in range(4):
441
442 x = newX + newPiece.x(i)
443 y = newY - newPiece.y(i)
444
445 if x < 0 or x >= Board.BoardWidth or y < 0 or y >= Board.BoardHeight:
446 return False
447
448 if self.shapeAt(x, y) != Tetrominoes.NoShape:
449 return False
450
451 self.curPiece = newPiece
452 self.curX = newX
453 self.curY = newY
454 self.Refresh()
455
456 return True
457
458
459 def drawSquare(self, dc, x, y, shape):
460 """
461 ...
462 """
463
464 colors = ['#000000', '#CC6666', '#66CC66', '#6666CC',
465 '#CCCC66', '#CC66CC', '#66CCCC', '#DAAA00']
466
467 light = ['#000000', '#F89FAB', '#79FC79', '#7979FC',
468 '#FCFC79', '#FC79FC', '#79FCFC', '#FCC600']
469
470 dark = ['#000000', '#803C3B', '#3B803B', '#3B3B80',
471 '#80803B', '#803B80', '#3B8080', '#806200']
472
473 pen = wx.Pen(light[shape])
474 pen.SetCap(wx.CAP_PROJECTING)
475 dc.SetPen(pen)
476
477 dc.DrawLine(x, y + self.squareHeight() - 1, x, y)
478 dc.DrawLine(x, y, x + self.squareWidth() - 1, y)
479
480 darkpen = wx.Pen(dark[shape])
481 darkpen.SetCap(wx.CAP_PROJECTING)
482 dc.SetPen(darkpen)
483
484 dc.DrawLine(x + 1, y + self.squareHeight() - 1,
485 x + self.squareWidth() - 1, y + self.squareHeight() - 1)
486 dc.DrawLine(x + self.squareWidth() - 1,
487 y + self.squareHeight() - 1, x + self.squareWidth() - 1, y + 1)
488
489 dc.SetPen(wx.TRANSPARENT_PEN)
490 dc.SetBrush(wx.Brush(colors[shape]))
491 dc.DrawRectangle(x + 1, y + 1, self.squareWidth() - 2,
492 self.squareHeight() - 2)
493
494 #---------------------------------------------------------------------------
495
496 class Tetrominoes(object):
497 """
498 ...
499 """
500
501 NoShape = 0
502 ZShape = 1
503 SShape = 2
504 LineShape = 3
505 TShape = 4
506 SquareShape = 5
507 LShape = 6
508 MirroredLShape = 7
509
510 #---------------------------------------------------------------------------
511
512 class Shape(object):
513 """
514 ...
515 """
516
517 coordsTable = (
518 ((0, 0), (0, 0), (0, 0), (0, 0)),
519 ((0, -1), (0, 0), (-1, 0), (-1, 1)),
520 ((0, -1), (0, 0), (1, 0), (1, 1)),
521 ((0, -1), (0, 0), (0, 1), (0, 2)),
522 ((-1, 0), (0, 0), (1, 0), (0, 1)),
523 ((0, 0), (1, 0), (0, 1), (1, 1)),
524 ((-1, -1), (0, -1), (0, 0), (0, 1)),
525 ((1, -1), (0, -1), (0, 0), (0, 1))
526 )
527
528 def __init__(self):
529 self.coords = [[0,0] for i in range(4)]
530 self.pieceShape = Tetrominoes.NoShape
531
532 self.setShape(Tetrominoes.NoShape)
533
534 #-----------------------------------------------------------------------
535
536 def shape(self):
537 """
538 ...
539 """
540
541 return self.pieceShape
542
543
544 def setShape(self, shape):
545 """
546 ...
547 """
548
549 table = Shape.coordsTable[shape]
550 for i in range(4):
551 for j in range(2):
552 self.coords[i][j] = table[i][j]
553
554 self.pieceShape = shape
555
556
557 def setRandomShape(self):
558 """
559 ...
560 """
561
562 self.setShape(random.randint(1, 7))
563
564
565 def x(self, index):
566 """
567 ...
568 """
569
570 return self.coords[index][0]
571
572
573 def y(self, index):
574 """
575 ...
576 """
577
578 return self.coords[index][1]
579
580
581 def setX(self, index, x):
582 """
583 ...
584 """
585
586 self.coords[index][0] = x
587
588
589 def setY(self, index, y):
590 """
591 ...
592 """
593
594 self.coords[index][1] = y
595
596
597 def minX(self):
598 """
599 ...
600 """
601
602 m = self.coords[0][0]
603 for i in range(4):
604 m = min(m, self.coords[i][0])
605
606 return m
607
608
609 def maxX(self):
610 """
611 ...
612 """
613
614 m = self.coords[0][0]
615 for i in range(4):
616 m = max(m, self.coords[i][0])
617
618 return m
619
620
621 def minY(self):
622 """
623 ...
624 """
625
626 m = self.coords[0][1]
627 for i in range(4):
628 m = min(m, self.coords[i][1])
629
630 return m
631
632
633 def maxY(self):
634 """
635 ...
636 """
637
638 m = self.coords[0][1]
639
640 for i in range(4):
641 m = max(m, self.coords[i][1])
642
643 return m
644
645
646 def rotatedLeft(self):
647 """
648 ...
649 """
650
651 if self.pieceShape == Tetrominoes.SquareShape:
652 return self
653
654 result = Shape()
655 result.pieceShape = self.pieceShape
656
657 for i in range(4):
658 result.setX(i, self.y(i))
659 result.setY(i, -self.x(i))
660
661 return result
662
663
664 def rotatedRight(self):
665 """
666 ...
667 """
668
669 if self.pieceShape == Tetrominoes.SquareShape:
670 return self
671
672 result = Shape()
673 result.pieceShape = self.pieceShape
674
675 for i in range(4):
676 result.setX(i, -self.y(i))
677 result.setY(i, self.x(i))
678
679 return result
680
681 #---------------------------------------------------------------------------
682
683 def main():
684 app = wx.App()
685 ex = Tetris(None)
686 ex.Show(True)
687 app.MainLoop()
688
689 #---------------------------------------------------------------------------
690
691 if __name__ == '__main__':
692 main()
Radar graph
1 # radar_graph.py
2
3 import wx
4 import math
5 import random
6
7 # class MyRadarGraph
8 # class MyFrame
9 # class MyApp
10
11 #---------------------------------------------------------------------------
12
13 class MyRadarGraph(wx.Window):
14 """
15 A simple radar graph that plots a collection of values in the
16 range of 0-100 onto a polar coordinate system designed to easily
17 show outliers, etc. You might use this kind of graph to monitor
18 some sort of resource allocation metrics, and a quick glance at
19 the graph can tell you when conditions are good (within some
20 accepted tolerance level) or approaching critical levels (total
21 resource consumption).
22 """
23 def __init__(self, parent, title, labels):
24 wx.Window.__init__(self, parent)
25
26 self.title = title
27 self.labels = labels
28 self.data = [0.0] * len(labels)
29 self.titleFont = wx.Font(14, wx.SWISS, wx.NORMAL, wx.BOLD)
30 self.labelFont = wx.Font(10, wx.SWISS, wx.NORMAL, wx.NORMAL)
31
32 self.InitBuffer()
33
34 self.Bind(wx.EVT_SIZE, self.OnSize)
35 self.Bind(wx.EVT_PAINT, self.OnPaint)
36
37 #-----------------------------------------------------------------------
38
39 def OnSize(self, evt):
40 """
41 When the window size changes we need a new buffer.
42 """
43
44 self.InitBuffer()
45
46
47 def OnPaint(self, evt):
48 """
49 This automatically Blits self.buffer to a wx.PaintDC when
50 the dc is destroyed, and so nothing else needs done.
51 """
52
53 dc = wx.BufferedPaintDC(self, self.buffer)
54
55
56 def InitBuffer(self):
57 """
58 Create the buffer bitmap to be the same size as the window,
59 then draw our graph to it. Since we use wx.BufferedDC
60 whatever is drawn to the buffer is also drawn to the window.
61 """
62
63 w, h = self.GetClientSize()
64 self.buffer = wx.Bitmap(w, h)
65 pdc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
66 dc = wx.GCDC(pdc)
67 self.DrawGraph(dc)
68
69
70 def GetData(self):
71 """
72 ...
73 """
74
75 return self.data
76
77
78 def SetData(self, newData):
79 """
80 ...
81 """
82
83 assert len(newData) == len(self.data)
84 self.data = newData[:]
85
86 # The data has changed, so update the buffer and the window.
87 pdc = wx.BufferedDC(wx.ClientDC(self), self.buffer)
88 dc = wx.GCDC(pdc)
89 self.DrawGraph(dc)
90
91
92 def PolarToCartesian(self, radius, angle, cx, cy):
93 """
94 ...
95 """
96
97 x = radius * math.cos(math.radians(angle))
98 y = radius * math.sin(math.radians(angle))
99 return (int(cx+x), int(cy-y))
100
101
102 def DrawGraph(self, dc):
103 """
104 ...
105 """
106
107 spacer = 10
108 scaledmax = 150.0
109
110 dc.SetBackground(wx.Brush(self.GetBackgroundColour()))
111 dc.Clear()
112 dw, dh = dc.GetSize()
113
114 # Find out where to draw the title and do it.
115 dc.SetFont(self.titleFont)
116 tw, th = dc.GetTextExtent(self.title)
117 dc.DrawText(self.title, int((dw-tw)/2), int(spacer))
118
119 # Find the center of the space below the title.
120 th = th + 2*spacer
121 cx = dw/2
122 cy = (dh-th)/2 + th
123
124 # Calculate a scale factor to use for drawing the graph
125 # based on the minimum available width or height.
126 mindim = min(cx, (dh-th)/2)
127 scale = mindim/scaledmax
128
129 # Draw the graph axis and "bulls-eye" with rings
130 # at scaled 25, 50, 75 and 100 positions.
131 dc.SetPen(wx.Pen("black", 1))
132 dc.SetBrush(wx.TRANSPARENT_BRUSH)
133 dc.DrawCircle(int(cx), int(cy), int(25*scale))
134 dc.DrawCircle(int(cx), int(cy), int(50*scale))
135 dc.DrawCircle(int(cx), int(cy), int(75*scale))
136 dc.DrawCircle(int(cx), int(cy), int(100*scale))
137
138 dc.SetPen(wx.Pen("black", 2))
139 dc.DrawLine(int(cx-110*scale), int(cy), int(cx+110*scale), int(cy))
140 dc.DrawLine(int(cx), int(cy-110*scale), int(cx), int(cy+110*scale))
141
142 # Now find the coordinates for each data point,
143 # draw the labels, and find the max data point.
144 dc.SetFont(self.labelFont)
145 maxval = 0
146 angle = 0
147 polypoints = []
148 for i, label in enumerate(self.labels):
149 val = self.data[i]
150 point = self.PolarToCartesian(int(val*scale), int(angle), int(cx), int(cy))
151 polypoints.append(point)
152 print(polypoints)
153 x, y = self.PolarToCartesian(int(125*scale), int(angle), int(cx), int(cy))
154 dc.DrawText(str(label), int(x), int(y))
155 if val > maxval:
156 maxval = val
157 angle = (int(angle) + int(360/len(self.labels)))
158
159 # Set the brush color based on the max value
160 # (green is good, red is bad).
161 c = "forest green"
162 if maxval > 70:
163 c = "yellow"
164 if maxval > 95:
165 c = "red"
166
167 # Finally, draw the plot data as a filled polygon.
168 dc.SetBrush(wx.Brush(c))
169 dc.SetPen(wx.Pen("navy", 3))
170 dc.DrawPolygon(polypoints)
171
172 #---------------------------------------------------------------------------
173
174 class MyFrame(wx.Frame):
175 def __init__(self):
176 wx.Frame.__init__(self, None,
177 title="Radar graph",
178 size=(480, 480))
179
180 self.SetIcon(wx.Icon("wxwin.ico"))
181 self.SetMinSize((320, 320))
182
183 self.plot = MyRadarGraph(self, """Sample "Radar" Plot""",
184 ["A", "B", "C", "D", "E", "F", "G", "H"])
185
186 # Set some random initial data values.
187 data = []
188 for d in self.plot.GetData():
189 data.append(random.randint(0, 75))
190 self.plot.SetData(data)
191
192 # Create a timer to update the data values.
193 self.Bind(wx.EVT_TIMER, self.OnTimeout)
194 self.timer = wx.Timer(self)
195 self.timer.Start(500)
196
197 #-----------------------------------------------------------------------
198
199 def OnTimeout(self, evt):
200 """
201 Simulate the positive or negative growth of each data value.
202 """
203
204 data = []
205 for d in self.plot.GetData():
206 val = d + random.uniform(-5, 5)
207 if val < 0:
208 val = 0
209 if val > 110:
210 val = 110
211 data.append(val)
212 self.plot.SetData(data)
213
214 #---------------------------------------------------------------------------
215
216 class MyApp(wx.App):
217 def OnInit(self):
218
219 #------------
220
221 frame = MyFrame()
222 self.SetTopWindow(frame)
223 frame.Show(True)
224
225 return True
226
227 #---------------------------------------------------------------------------
228
229 def main():
230 app = MyApp(redirect=False)
231 app.MainLoop()
232
233 #---------------------------------------------------------------------------
234
235 if __name__ == "__main__" :
236 main()
Lines
1 # lines.py
2
3 import wx
4 import random
5
6 # class MyFrame
7 # class MyApp
8
9 #---------------------------------------------------------------------------
10
11 class MyFrame(wx.Frame):
12 """
13 This window displays a button.
14 """
15 def __init__(self, parent, id, title):
16 wx.Frame.__init__(self, None, -1, title,
17 wx.DefaultPosition,
18 size=(400, 400),
19 style=wx.DEFAULT_FRAME_STYLE |
20 wx.NO_FULL_REPAINT_ON_RESIZE)
21
22 self.SetIcon(wx.Icon('wxwin.ico'))
23 self.SetBackgroundColour(wx.WHITE)
24 self.SetMinSize((220, 220))
25
26 self.Bind(wx.EVT_CLOSE, self.OnQuit)
27
28 self.Bind(wx.EVT_TIMER, self.OnTimer)
29 self.Bind(wx.EVT_SIZE, self.BuildImage)
30
31 self.Numtimer = 0
32 self.NumLines = 100
33
34 self.timer = wx.Timer(self)
35 self.BuildImage()
36 self.timer.Start(100)
37
38 #-----------------------------------------------------------------------
39
40 def OnQuit(self, event):
41 """
42 ...
43 """
44
45 self.Destroy()
46
47
48 def BuildImage(self, event=None):
49 """
50 ...
51 """
52
53 Size = self.GetClientSize()
54
55 # Make new offscreen bitmap : this bitmap will always have the
56 # current drawing in it, so it can be used to save the image to
57 # a file, or whatever.
58 print("making new buffer :", Size)
59 self._Buffer = wx.Bitmap(Size[0], Size[1])
60
61 dc = wx.MemoryDC()
62 dc.SelectObject(self._Buffer)
63 dc = wx.GCDC(dc)
64
65 self.Lines = []
66
67 for i in range(self.NumLines):
68 x1, y1, x2, y2 = (random.randint(1, max(Size)),
69 random.randint(1, max(Size)),
70 random.randint(1, max(Size)),
71 random.randint(1, max(Size)))
72
73 color = self.random_color()
74 self.Lines.append([color, (x1 ,y1, x2, y2)])
75
76 dc.Clear()
77
78 for line in self.Lines:
79 dc.SetPen(wx.Pen(line[0], 2))
80 dc.DrawLine(*line[1])
81
82
83 def OnTimer(self,event):
84 """
85 ...
86 """
87
88 self.Numtimer += 1
89 print("Timer fired : %i times" % self.Numtimer)
90
91 # Change one color.
92 self.Lines[random.randrange(self.NumLines)][0] = self.random_color()
93
94 # Update the screen.
95 dc = wx.MemoryDC()
96 dc.SelectObject(self._Buffer)
97 dc = wx.GCDC(dc)
98 dc.Clear()
99
100 for line in self.Lines:
101 dc.SetPen(wx.Pen(line[0], 2))
102 dc.DrawLine(*line[1])
103
104 del dc
105
106 wx.ClientDC(self).DrawBitmap(self._Buffer, 0, 0)
107
108
109 def random_color(self):
110 """
111 ...
112 """
113
114 col = wx.Colour(random.randrange(255),
115 random.randrange(255),
116 random.randrange(255))
117
118 return col
119
120 #---------------------------------------------------------------------------
121
122 class MyApp(wx.App):
123 """
124 ...
125 """
126 def OnInit(self):
127
128 #------------
129
130 frame = MyFrame(None, -1, title="Lines")
131 self.SetTopWindow(frame)
132 frame.Show(True)
133
134 return True
135
136 #---------------------------------------------------------------------------
137
138 def main():
139 app = MyApp(False)
140 app.MainLoop()
141
142 #---------------------------------------------------------------------------
143
144 if __name__ == "__main__" :
145 main()
Buffered with controls
1 # buffered_with_controls.py
2
3 import random
4 import wx
5 import wx.lib.buttons as buttons
6
7 # In ms.
8 REFRESH_RATE = 1000
9
10 # class MyBufferedWindow
11 # class MyDrawWindow
12 # class MyFrame
13 # class MyApp
14
15 #-------------------------------------------------------------------------------
16
17 class MyBufferedWindow(wx.Window):
18 def __init__(self, parent, id,
19 pos=wx.DefaultPosition,
20 size=wx.DefaultSize,
21 style=wx.NO_FULL_REPAINT_ON_RESIZE):
22 wx.Window.__init__(self, parent, id, pos, size,
23 style | wx.CLIP_CHILDREN)
24
25 self.Bind(wx.EVT_PAINT, self.OnPaint)
26 self.Bind(wx.EVT_SIZE, self.OnSize)
27
28 # OnSize called to make sure the buffer is initialized.
29 # This might result in OnSize getting called twice on some
30 # platforms at initialization, but little harm done.
31 self.OnSize(None)
32
33 #-------------------------------------------------------------------
34
35 def Draw(self, dc):
36 """
37 Just here as a place holder.
38 This method should be over-ridden when sub-classed.
39 """
40
41 pass
42
43
44 def OnPaint(self, event):
45 """
46 All that is needed here is to draw the buffer to screen.
47 """
48
49 dc = wx.BufferedPaintDC(self, self._Buffer)
50
51
52 def OnSize(self, event):
53 """
54 ...
55 """
56
57 # The Buffer init is done here, to make sure the
58 # buffer is always the same size as the Window.
59 self.Width, self.Height = self.GetClientSize()
60
61 # Make new off screen bitmap : this bitmap will always have the
62 # current drawing in it, so it can be used to save the image to
63 # a file, or whatever.
64 self._Buffer = wx.Bitmap(self.Width, self.Height)
65 self.UpdateDrawing()
66
67
68 def SaveToFile(self,FileName, FileType):
69 """
70 This will save the contents of the buffer
71 to the specified file. See the wxWindows docs
72 for wx.Bitmap::SaveFile for the details.
73 """
74
75 self._Buffer.SaveFile(FileName, FileType)
76
77
78 def UpdateDrawing(self):
79 """
80 This would get called if the drawing needed to change, for whatever reason.
81 The idea here is that the drawing is based on some data generated
82 elsewhere in the system. If that data changes, the drawing needs to
83 be updated.
84 """
85
86 dc = wx.MemoryDC()
87 dc.SelectObject(self._Buffer)
88 self.Draw(dc)
89 # This forces a Paint event, so the screen gets updated.
90 self.Refresh()
91 # If it's not getting updated fast enough, this should force it.
92 # self.Update()
93
94 #-------------------------------------------------------------------------------
95
96 class MyDrawWindow(MyBufferedWindow):
97 def __init__(self, parent, id=-1):
98 """
99 Any data the Draw() function needs must be initialized before
100 calling MyBufferedWindow.__init__, as it will call the Draw
101 function.
102 """
103
104 self.DrawData = {}
105
106 MyBufferedWindow.__init__(self, parent, id)
107
108 #------------
109
110 # Simplified init method.
111 self.BtnNormal()
112 self.BtnBitmap()
113 self.CheckB()
114
115 #-------------------------------------------------------------------
116
117 def Draw(self, dc):
118 """
119 ...
120 """
121
122 dc.SetBackground(wx.Brush("Gray"))
123 dc.Clear() # Make sure you clear the bitmap !
124
125 # Here's the actual drawing code.
126 for key,data in self.DrawData.items():
127 if key == "Rectangles":
128 dc.SetBrush(wx.YELLOW_BRUSH)
129 dc.SetPen(wx.Pen("VIOLET", 4))
130 for r in data:
131 dc.DrawRectangle(*r)
132
133 elif key == "Ellipses":
134 dc.SetBrush(wx.Brush("GREEN"))
135 dc.SetPen(wx.Pen("CADET BLUE", 2))
136 for r in data:
137 dc.DrawEllipse(*r)
138
139 elif key == "Polygons":
140 dc.SetBrush(wx.Brush("SALMON"))
141 dc.SetPen(wx.Pen("VIOLET RED", 4))
142 for r in data:
143 dc.DrawPolygon(r)
144
145
146 def BtnNormal(self):
147 """
148 ...
149 """
150
151 self.btnNormal = wx.Button(self, 600, "normal btn", pos=(100, 100))
152 self.Bind(wx.EVT_BUTTON, self.OnNormalButton, id=600)
153
154
155 def OnNormalButton(self,event=None):
156 """
157 ...
158 """
159
160 print("Normal Button Clicked")
161
162
163 def BtnBitmap(self):
164 """
165 ...
166 """
167
168 # Only bitmap buttons seam to work.
169 self.buttonTest2 = buttons.GenBitmapTextButton(self, 700, None,
170 "bitmap btn",
171 pos=(250, 250))
172 self.Bind(wx.EVT_BUTTON, self.OnBitmapButton, id=700)
173
174
175 def OnBitmapButton(self,event=None):
176 """
177 ...
178 """
179
180 print("Bitmap Button Clicked")
181
182
183 def CheckB(self):
184 """
185 ...
186 """
187
188 self.checkB = wx.CheckBox(self, 800, "test", pos=(300, 50))
189
190 #-------------------------------------------------------------------------------
191
192 class MyFrame(wx.Frame):
193 def __init__(self):
194 wx.Frame.__init__(self, None, -1,
195 "Double Buffered with controls",
196 wx.DefaultPosition,
197 size=(420, 420),
198 style=wx.DEFAULT_FRAME_STYLE |
199 wx.NO_FULL_REPAINT_ON_RESIZE)
200
201 #------------
202
203 self.SetIcon(wx.Icon('wxwin.ico'))
204 self.SetMinSize((320, 320))
205
206 #------------
207
208 # Delete flicker.
209 if wx.Platform == "__WXMSW__":
210 self.SetDoubleBuffered(True)
211
212 #------------
213
214 # Set up the menuBar.
215 menuBar = wx.MenuBar()
216
217 file_menu = wx.Menu()
218
219 file_menu.Append(500, "E&xit", "Terminate the program")
220 self.Bind(wx.EVT_MENU, self.OnQuit, id=500)
221 menuBar.Append(file_menu, "&File")
222
223 draw_menu = wx.Menu()
224
225 draw_menu.Append(400, "&New Drawing","Update the Drawing Data")
226 self.Bind(wx.EVT_MENU,self.NewDrawing, id=400)
227
228 draw_menu.Append(300, "&Save Drawing\tAlt-I", "")
229 self.Bind(wx.EVT_MENU, self.SaveToFile, id=300)
230
231 menuBar.Append(draw_menu, "&Draw")
232
233 draw_menu.Append(200, "&Start Periodic Update\tF5")
234 self.Bind(wx.EVT_MENU, self.StartUpdate, id=200)
235
236 self.SetMenuBar(menuBar)
237
238 draw_menu.Append(100, "&Stop Periodic Update\tF6")
239 self.Bind(wx.EVT_MENU, self.StopUpdate, id=100)
240
241 self.SetMenuBar(menuBar)
242
243 #------------
244
245 self.Window = MyDrawWindow(self)
246
247 #------------
248
249 # Now set up the timer.
250 self.timer = wx.Timer(self)
251 self.Bind(wx.EVT_TIMER, self.Notify)
252
253 #-------------------------------------------------------------------
254
255 def StartUpdate(self, event):
256 """
257 ...
258 """
259
260 # Time between events (in milliseconds).
261 self.timer.Start(1000)
262
263
264 def StopUpdate(self, event=None):
265 """
266 ...
267 """
268
269 self.timer.Stop()
270
271
272 def Notify(self, event):
273 """
274 ...
275 """
276
277 self.NewDrawing(None)
278
279
280 def OnQuit(self, event):
281 """
282 ...
283 """
284
285 self.Close(True)
286
287
288 def NewDrawing(self, event):
289 """
290 ...
291 """
292
293 self.Window.DrawData = self.MakeNewData()
294 self.Window.UpdateDrawing()
295
296
297 def SaveToFile(self, event):
298 """
299 ...
300 """
301
302 dlg = wx.FileDialog(self, "Choose a file name to save the image as a PNG",
303 defaultDir="",
304 defaultFile="",
305 wildcard="*.png",
306 style=wx.FD_SAVE)
307 if dlg.ShowModal() == wx.ID_OK:
308 self.Window.SaveToFile(dlg.GetPath(), wx.BITMAP_TYPE_PNG)
309 dlg.Destroy()
310
311
312 def MakeNewData(self):
313 """
314 This method makes some random data to draw things with.
315 """
316
317 MaxX, MaxY = self.Window.GetClientSize()
318 DrawData = {}
319
320 # Make some random rectangles.
321 l = []
322 for i in range(2):
323 w = random.randint(1, int(MaxX/2))
324 h = random.randint(1, int(MaxY/2))
325 x = random.randint(1, int(MaxX-w))
326 y = random.randint(1, int(MaxY-h))
327 l.append( (x,y,w,h) )
328 DrawData["Rectangles"] = l
329
330 return DrawData
331
332 #-------------------------------------------------------------------------------
333
334 class MyApp(wx.App):
335 def OnInit(self):
336 # Called so a PNG can be saved.
337 # wx.InitAllImageHandlers()
338 frame = MyFrame()
339 frame.Show(True)
340
341 # Initialize a drawing.
342 # It doesn't seem like this should be here, but the Frame does
343 # not get sized until Show() is called, so it doesn't work if
344 # it is put in the __init__ method.
345 frame.NewDrawing(None)
346
347 self.SetTopWindow(frame)
348
349 return True
350
351 #-------------------------------------------------------------------------------
352
353 if __name__ == "__main__":
354 app = MyApp(False)
355 app.MainLoop()
Balls
1 # balls.py
2
3 # https://bty.sakura.ne.jp/wp/archives/78
4
5 import wx
6 import wx.lib.mixins.listctrl as listmix
7
8 # class MyBall
9 # class MyFrame
10 # class MyApp
11
12 #---------------------------------------------------------------------------
13
14 class MyBall(object):
15 def __init__(self, panel, x, y, color, pen):
16
17 #------------
18
19 self.pos_x = x
20 self.pos_y = y
21 self.dir_x = 1
22 self.dir_y = 1
23 self.pos_dif = 20
24 self.pos_x_max, self.pos_y_max = panel.GetSize()
25 self.color = color
26 self.pen = pen
27
28 #-----------------------------------------------------------------------
29
30 def Move(self):
31 """
32 ...
33 """
34
35 if self.dir_x > 0:
36 self.pos_x += self.pos_dif
37 else:
38 self.pos_x -= self.pos_dif
39
40 if self.pos_x > self.pos_x_max:
41 self.pos_x -= self.pos_dif
42 self.dir_x = -1
43
44 if self.pos_x < 0:
45 self.pos_x += self.pos_dif
46 self.dir_x = 1
47
48 if self.dir_y > 0:
49 self.pos_y += self.pos_dif
50 else:
51 self.pos_y -= self.pos_dif
52
53 if self.pos_y > self.pos_y_max:
54 self.pos_y -= self.pos_dif
55 self.dir_y = -1
56
57 if self.pos_y < 0:
58 self.pos_y += self.pos_dif
59 self.dir_y = 1
60
61
62 def Draw(self, dc):
63 """
64 ...
65 """
66
67 dc.SetPen(wx.Pen(self.pen, 3))
68 dc.SetBrush(wx.Brush(self.color))
69 dc.DrawCircle(self.pos_x, self.pos_y, 20)
70
71 #---------------------------------------------------------------------------
72
73 class MyFrame(wx.Frame):
74 def __init__(self, title):
75 wx.Frame.__init__(self, None, -1,
76 title)
77
78 #------------
79
80 self.SetIcon(wx.Icon('wxwin.ico'))
81 self.SetMinSize((420, 240))
82
83 #------------
84
85 self.panel = wx.Panel(self, size=(404, 200))
86 self.panel.SetBackgroundColour('light gray')
87 self.Fit()
88
89 self.ball1 = MyBall(self.panel, 150, 100, 'red', 'black')
90 self.ball2 = MyBall(self.panel, 100, 50, 'blue', 'gray')
91 self.ball3 = MyBall(self.panel, 50, 150, 'green', 'blue')
92
93 self.Bind(wx.EVT_CLOSE, self.CloseWindow)
94
95 #------------
96
97 self.cdc = wx.ClientDC(self.panel)
98 w, h = self.panel.GetSize()
99 self.bmp = wx.Bitmap(w, h)
100
101 #------------
102
103 self.timer = wx.Timer(self)
104 self.Bind(wx.EVT_TIMER, self.OnTimer)
105 self.timer.Start(100)
106
107 #-----------------------------------------------------------------------
108
109 def CloseWindow(self, event):
110 """
111 ...
112 """
113
114 self.timer.Stop()
115 self.Destroy()
116 wx.Exit()
117
118
119 def OnTimer(self, event):
120 """
121 ...
122 """
123
124 bdc = wx.BufferedDC(self.cdc, self.bmp)
125 gcdc = wx.GCDC(bdc)
126 gcdc.SetBackground(wx.Brush("light gray"))
127 gcdc.Clear()
128
129 self.ball1.Move()
130 self.ball2.Move()
131 self.ball3.Move()
132
133 self.ball1.Draw(gcdc)
134 self.ball2.Draw(gcdc)
135 self.ball3.Draw(gcdc)
136
137 gcdc.DrawText("Moving Ball", 110, 70)
138 gcdc.DrawBitmap(self.bmp, 0, 0)
139
140 #---------------------------------------------------------------------------
141
142 class MyApp(wx.App):
143 def OnInit(self):
144
145 #------------
146
147 frame = MyFrame("Balls")
148 self.SetTopWindow(frame)
149 frame.Show(True)
150
151 return True
152
153 #---------------------------------------------------------------------------
154
155 def main():
156 app = MyApp(False)
157 app.MainLoop()
158
159 #---------------------------------------------------------------------------
160
161 if __name__ == "__main__" :
162 main()
Download source
Additional Information
Link :
http://zetcode.com/wxpython/thetetrisgame/
- - - - -
https://wiki.wxpython.org/TitleIndex
Thanks to
LionKimbro (bubbles_toy.py coding), Jan Bodnar (tetris.py coding), Robin Dunn / Noel Rappin (radar_graph.py coding), Chris Barker (lines / buffered_with_controls.py coding), Sakura (balls.py coding), the wxPython community...
About this page
Date(d/m/y) Person (bot) Comments :
22/12/19 - Ecco (Updated page for wxPython Phoenix).
05/01/20 - Ecco (Added examples for wxPython Phoenix).
Comments
- I love it! I've refactored the code and made it more OO, and also fixed an AssertionError on Windows. -- RobinDunn
- I'm honored! Thank you! -- LionKimbro
- This is a fantastic example of custom animation in wx. I've updated it to use AutoBufferedPaintDC, rather than explicitly using a double-buffered context, so that it gets optimal performance even on systems where double-buffering isn't needed (like OS X). -- JoeStrout