Drawing outside of OnPaint with wxWidgets

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

The 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)

Combination of Refresh() and 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:

Squared sun

You should get a copy of my slick ZeroBrane Studio IDE and follow me on twitter here.

Leave a comment

what will you say?
(required)
(required)

About

I am Paul Kulchenko.
I live in Kirkland, WA with my wife and three kids.
I do consulting as a software developer.
I study robotics and artificial intelligence.
I write books and open-source software.
I teach introductory computer science.
I develop a slick Lua IDE and debugger.

Recommended

Close