Sunday, December 26, 2004

wxPython woes

I've been trying to understand better how to use wxPython but I still have problems with doing "custom" double-buffering displays other than for simple programs. For example, the techniques used in the program below could be used as the basis for a simple pac-man like game. It works well, using the built-in buffered device contexts but I'd like to understand how to do my own buffered drawings on scrolled windows (It's "easy" to get things working with fixed-sized windows). For those interested, have a look at the onPaint() and drawImage() methods.

One of the problems that I am encountering is the incompleteness (for non-expert like me) of the available documentation on wxPython. So, I resolved to write my wxPython based programs with lots of comments in the hope that they will be useful to other people


""" This wxPython-based program illustrates:
* the use of foreground and background program-generated images;
* the use of cursor keys to move an image;
* the automatic scrolling of windows;
* double-buffering of images using "built-in" bufferedDC's.
Using the arrow-keys, we move a simple image (circle) on a checkered
background inside a scrolled-window, scrolling automatically as needed to
keep the circle always visible.
"""

import wx

custom_buffer = False

class CheckeredBackground(object):
"""Checkered-like image pattern defined so that we can better
visualise the moving canvas."""
# Note: there is no real need to define this as a class; it could
# have been simply defined as a function of MyCanvas below.
# Note 2: default values are provided but not used in this program.

def __init__(self, width=600, height=480, side=40):

#-- prepare to create bitmap image
self.image = wx.EmptyBitmap(width, height)

# Before we can "draw", we need to specify what the context will be.
#-- Device context (DC) will be computer memory
offDC = wx.MemoryDC()

#-- prepare to work with the image
offDC.SelectObject(self.image)

#-- select a background colour
offDC.SetBackground(wx.Brush("WHEAT"))

#-- "paint" over the entire object with the background colour
offDC.Clear()

#-- create the pattern in "separate" memory
nb_col = width//side
nb_row = height//side
squares = []
for i in range(0, nb_col, 2):
for j in range(0, nb_row, 2):
x = i*side
y = j*side
squares.append( (x, y, side, side) )
for i in range(1, nb_col, 2):
for j in range(1, nb_row, 2):
x = i*side
y = j*side
squares.append( (x, y, side, side) )

#-- Choose square outline colour
offDC.SetPen(wx.Pen("PALE GREEN", side/8))

#-- Choose square interior (fill) colour
offDC.SetBrush(wx.Brush("LIGHT STEEL BLUE"))

#-- "draw" the squares in DC memory
offDC.DrawRectangleList(squares)

#-- release bitmap image from drawing context. This amounts,
# as I understand, to undoing SelectObject().
del offDC

class MovingCircle(object):
"""Creates a simple circle on transparent background"""
# Note: there is no real need to define this as a class.
# It is done here for later re-use in other examples.
# Note 2: default values are provided but not used in this program.

def __init__(self, radius = 10, string_colour = "RED"):

#-- prepare to create bitmap image
self.image = wx.EmptyBitmap(2*radius, 2*radius)

#-- Device context (DC) will be computer memory
offDC = wx.MemoryDC()

#-- prepare to work with the image
offDC.SelectObject(self.image)

""" Preparing to draw a shape with a transparent background.
I thought that choosing a wx.Brush with a TRANSPARENT_BRUSH
colour, and clearing the DC with it would work - but it
does not. What does work is to select an "unused" colour
for the background and set up a mask with it."""
#-- choose an "unusual" background colour ...
offDC.SetBackground(wx.Brush(wx.Colour(2,2,2), wx.SOLID))

#-- ... and set it everywhere
offDC.Clear()

#-- different colour for object
offDC.SetPen(wx.Pen(string_colour, 1)) # outline
offDC.SetBrush(wx.Brush(string_colour)) # interior

offDC.DrawCircle(radius, radius, radius)# center: (x, y) and radius.

#-- release bitmap image from drawing context to process it further
del offDC

#-- set up a mask with our "unusual" colour
mask = wx.Mask(self.image, wx.Colour(2,2,2))
self.image.SetMask(mask)
#-- only regions where colour != wx.Colour(2,2,2) survives.


class MyCanvas(wx.ScrolledWindow):
def __init__(self, parent, id = -1, size = wx.DefaultSize):
wx.ScrolledWindow.__init__(self, parent, id, (0, 0), size=size,
style=wx.SUNKEN_BORDER)
# sets dimensions so that image will be larger than window,
# and scrolling can occur.
self.maxWidth = 1200
self.maxHeight = 800

# Set the size of the total window, of which only a small part
# will be displayed; apparently SetVirtualSize needs
# a single (tuple) argument, which explains the double (( )).
self.SetVirtualSize((self.maxWidth, self.maxHeight))

# Set the scrolling rate; use same value in both horizontal and
scrollRate = 20 # vertical directions.
self.SetScrollRate(scrollRate, scrollRate)

# Create the background image
side = 40
self.background = CheckeredBackground(self.maxWidth,
self.maxHeight, side)

# Create small foreground images; normally, such an image might
# be imported from a file
self.radius = 40
self.red_circle = MovingCircle(self.radius, "RED")

# sets its position (used later)
self.circle_x = 0 # position of top left corner of enclosing box
self.circle_y = 0

# bind the key events that will be used to move the small image
self.bindEvents()

# Initialize the buffer bitmap. No real DC is needed at this point.
self.buffer = wx.EmptyBitmap(self.maxWidth, self.maxHeight)
self.drawImage()

def bindEvents(self):
# use the old style [still works!] instead of self.Bind(evt, fn)
wx.EVT_PAINT(self, self.OnPaint)
wx.EVT_CHAR(self, self.MyArrowKeys)

##-- onPaint() and drawImage() are the two methods I'd like to know how
##-- to do with custom-based buffered DCs.

def OnPaint(self, event):
if custom_buffer:
pass
# I haven't been able to figure out how to do this
else:
dc = wx.BufferedPaintDC(self, self.buffer)

def drawImage(self):
if custom_buffer:
pass
# I haven't been able to figure out how to do this.
else:
dc = wx.BufferedDC(None, self.buffer)
dc.Clear()
dc.BeginDrawing()
# First copy the background image onto the buffer
dc.DrawBitmap(self.background.image, 0, 0, True)
# Next, superimpose the foreground image
dc.DrawBitmap(self.red_circle.image, self.circle_x,
self.circle_y, True)
dc.EndDrawing()
del dc

def MoveCircle(self, x, y):
self.circle_x += x
self.circle_y += y

#-- Prevent the circle from moving out of bounds
# (of the full background image, not the visible part.)
if self.circle_x < circle_x =" 0" circle_y =" 0"> self.maxWidth - 2*self.radius:
self.circle_x = self.maxWidth - 2*self.radius
if self.circle_y > self.maxHeight - 2*self.radius:
self.circle_y = self.maxHeight - 2*self.radius

hidden = 40 # approximate space hidden under scrollbars

# determine the position of top left visible window in
# "scrollrate" units
xView, yView = self.GetViewStart()

# corresponding amount of pixel per "scroll"
xDelta, yDelta = self.GetScrollPixelsPerUnit()

# size of wisible window
width, height = self.GetSizeTuple()

#-- Determine if window needs to be scrolled so that object
# remains visible. Assume that the object fits entirely
# in the visible view.
if self.circle_x < xview =" max(0,"> xView*xDelta + width:
xView = (self.circle_x + 2*self.radius + hidden - width)/xDelta

if self.circle_y < yview =" max(0,"> yView*yDelta + height:
yView = (self.circle_y + 2*self.radius + hidden - height)/yDelta

self.Scroll(xView, yView)

self.drawImage()
self.Refresh(False)

def MyArrowKeys(self, event):
code = event.KeyCode()
if code == wx.WXK_UP:
self.MoveCircle(0, -10) # up on screen is negative y-direction
elif code == wx.WXK_LEFT:
self.MoveCircle(-10, 0)
elif code == wx.WXK_RIGHT:
self.MoveCircle(10, 0)
elif code == wx.WXK_DOWN:
self.MoveCircle(0, 10)
else:
pass # ignore all other keys pressed


class AnimationFrame(wx.Frame):
def __init__(self, parent):
wx.Frame.__init__(self, parent, -1, "Animation Frame", size=(400, 300),
style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE)
doodle = MyCanvas(self, -1)

#----------------------------------------------------------------------

if __name__ == '__main__':
app = wx.PySimpleApp()
frame = AnimationFrame(None)
frame.Show(True)
app.MainLoop()



3 comments:

Anonymous said...

Perhaps you could add it too the wxpyhton Cookbook.
http://wiki.wxpython.org/index.cgi/RecipesImagesAndGraphics

I never used DC, but your code and comments were verry readable.

Anonymous said...

Your code show 3 (transcript) errors:

- on line 187:

if self.circle_x < circle_x =" 0" circle_y =" 0"> self.maxWidth - 2*self.radius:

- on line 207:

if self.circle_x < xview =" max(0,"> xView*xDelta + width:

- on line 210:

if self.circle_y < yview =" max(0,"> yView*yDelta + height:

taw said...

Thanks a lot, the example code really saved me. Here are some screenshots. ;-)