As I have been working on the Turtle graphics library for my son, I realized that the traditional model with OnPaint event that draws everything that needs to be drawn is not very suitable for what I wanted to have. I wanted to be able to draw when I need to and avoid re-drawing everything that has been drawn so far (or at least do it in an easy way). After a bit of reading and experimenting with wxWidgets, it turned out to be not that difficult. There are four main ideas.
Create a bitmap to be drawn on.
local bitmap local mdc = wx.wxMemoryDC() local function reset () local size = frame:GetClientSize() local w,h = size:GetWidth(),size:GetHeight() bitmap = wx.wxBitmap(w,h) mdc:SetDeviceOrigin(w/2, h/2) mdc:SelectObject(bitmap) mdc:Clear() mdc:SetPen(wx.wxBLACK_PEN) mdc:SetFont(wx.wxSWISS_FONT) -- thin TrueType font mdc:SelectObject(wx.wxNullBitmap) end
The code is straightforward: it creates a bitmap using the client size of the frame (the client size excludes border and caption areas); all the drawing is done on that bitmap.
Draw on the bitmap using wxMemoryDC.
local bitmap local mdc = wx.wxMemoryDC() local function line (x1, y1, x2, y2) mdc:SelectObject(bitmap) mdc:DrawLine(x1, y1, x2, y2) mdc:SelectObject(wx.wxNullBitmap) if autoUpdate then updt() end end
SelectObject method selects the bitmap and then releases it by selecting the NullBitmap after the drawing operation is completed to allow the object to be cleared when needed.
Register handlers for PAINT and ERASE_BACKGROUND events.
function OnPaint(event) -- must always create a wxPaintDC in a wxEVT_PAINT handler local dc = wx.wxPaintDC(frame) dc:DrawBitmap(bitmap, 0, 0, true) dc:delete() -- ALWAYS delete() any wxDCs created when done end frame:Connect(wx.wxEVT_PAINT, OnPaint) frame:Connect(wx.wxEVT_ERASE_BACKGROUND, function () end) -- do nothing
OnPaint event handler is very simple: it only draws the bitmap that already has all the content we want to display on the screen. The
ERASE_BACKGROUND event handler does nothing as we take care of redrawing everything ourselves. If this is not done, then the user is likely to see flicker that will be created by repeated clearing and redrawing the same frame.
Handle applications events to avoid "busyness".
local autoUpdate = true local function updt (update) local curr = autoUpdate if update ~= nil then autoUpdate = update end frame:Refresh() frame:Update() wx.wxGetApp():MainLoop() return curr end local exit = true frame:Connect(wx.wxEVT_IDLE, function () if exit then wx.wxGetApp():ExitMainLoop() end end)
Update() calls forces refresh of the content, while calling
MainLoop() allows the application to process all pending events (thus avoiding busyness). It seems like
wx.wxSafeYield(frame) could potentially be used instead of
MainLoop() for the same purpose, but I could not avoid the "hourglass" cursor and the window was slow to respond to mouse events.
Note that the
MainLoop() call does not indefinitely block the application as the
IDLE event handler immediately exits the main loop when called. As the
IDLE event is called only after all the pending events have been processed, this guarantees that the application continues respond to events while drawing the content we need. The "exit" check is only used for the final call of the
MainLoop() function when we do want to block until the user closes the application.
The complete code is available here. Here is the image it generates: