Making a render loop
From WxWiki
While wxWidgets is not an ideal tool for something like a game that requires a high performance render loop, you might still want to use a render loop in your application.
This article will attempt to give examples of how it can be done. Note that there may be more possibilities than explained here, I will try to extend this article in the future.
Contents |
What not to do
The following ways should not be used, as they can result in obscure errors or crashes that will be very difficult to debug
- Use a loop
// wxWidgets, like all GUI libraries, handles events in an event loop. This event loop // is NOT magical, i.e. when the program is looping in your loop then the wxWidgets event // loop does not get to execute and the app freezes. Also, calling "sleep" makes the thread // as a whole sleep, and that includes wxWidgets. wxWidgets will not continue executing // magically if you sleep the main thread. while(true) { render(); // or Refresh() sleep(1); }
- Use wxYield() in a loop
// Better than other example above, and may work, but not so good idea. // it's not elegant and can create re-entrency issues while(1) { render(); // or Refresh() wxYield(); }
- Use a loop in a separate thread (bad because wxWidgets requires all GUI calls to happen in the main thread). However you can use a loop in a thread that sends events to the main thread regularly, however this is just a more complicated version of the wxTimer example posted below.
Other things to check for if you get weird results
- Don't use wxPaintDC outside paint events (use wxClientDC instead.)
- If you catch paint events, you must create a wxPaintDC inside them. Failing to do so will result in the event not being handled properly and weird behavior can ensue.
Using idle events
Pros :
- doesn't hog the system, as it renders only when it can
- one of the fastest ways possible in wx (on my MacBook 2GHz I get around 3500 fps with the code below)
Cons :
- framerate is not necessarly regular
- can stop rendering when you do something else, for instance open a menu
#include "wx/sizer.h" #include "wx/wx.h" class BasicDrawPane : public wxPanel { public: BasicDrawPane(wxFrame* parent); void paintEvent(wxPaintEvent& evt); void paintNow(); void render( wxDC& dc ); DECLARE_EVENT_TABLE() }; class MyFrame; class MyApp: public wxApp { bool render_loop_on; bool OnInit(); void onIdle(wxIdleEvent& evt); MyFrame* frame; BasicDrawPane* drawPane; public: void activateRenderLoop(bool on); }; IMPLEMENT_APP(MyApp) class MyFrame : public wxFrame { public: MyFrame() : wxFrame((wxFrame *)NULL, -1, wxT("Hello wxDC"), wxPoint(50,50), wxSize(400,200)) { } void onClose(wxCloseEvent& evt) { wxGetApp().activateRenderLoop(false); evt.Skip(); // don't stop event, we still want window to close } DECLARE_EVENT_TABLE() }; BEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_CLOSE(MyFrame::onClose) END_EVENT_TABLE() bool MyApp::OnInit() { render_loop_on = false; wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); frame = new MyFrame(); drawPane = new BasicDrawPane( frame ); sizer->Add(drawPane, 1, wxEXPAND); frame->SetSizer(sizer); frame->Show(); activateRenderLoop(true); return true; } void MyApp::activateRenderLoop(bool on) { if(on && !render_loop_on) { Connect( wxID_ANY, wxEVT_IDLE, wxIdleEventHandler(MyApp::onIdle) ); render_loop_on = true; } else if(!on && render_loop_on) { Disconnect( wxEVT_IDLE, wxIdleEventHandler(MyApp::onIdle) ); render_loop_on = false; } } void MyApp::onIdle(wxIdleEvent& evt) { if(render_loop_on) { drawPane->paintNow(); evt.RequestMore(); // render continuously, not only once on idle } } BEGIN_EVENT_TABLE(BasicDrawPane, wxPanel) EVT_PAINT(BasicDrawPane::paintEvent) END_EVENT_TABLE() BasicDrawPane::BasicDrawPane(wxFrame* parent) : wxPanel(parent) { } void BasicDrawPane::paintEvent(wxPaintEvent& evt) { wxPaintDC dc(this); render(dc); } void BasicDrawPane::paintNow() { wxClientDC dc(this); render(dc); } void BasicDrawPane::render( wxDC& dc ) { static int y = 0; static int y_speed = 2; y += y_speed; if(y<0) y_speed = 2; if(y>200) y_speed = -2; dc.SetBackground( *wxWHITE_BRUSH ); dc.Clear(); dc.DrawText(wxT("Testing"), 40, y); }
Using a timer
Pros :
- Regular framerate
- Doesn't stop like idle events when you do something else, like open a menu
Cons :
- Not as fast as idle events
- Implementation quality depends on your platform; some platforms provide better timers than others.
#include <wx/sizer.h> #include <wx/wx.h> #include <wx/timer.h> class BasicDrawPane; class RenderTimer : public wxTimer { BasicDrawPane* pane; public: RenderTimer(BasicDrawPane* pane); void Notify(); void start(); }; class BasicDrawPane : public wxPanel { public: BasicDrawPane(wxFrame* parent); void paintEvent(wxPaintEvent& evt); void paintNow(); void render( wxDC& dc ); DECLARE_EVENT_TABLE() }; class MyFrame; class MyApp: public wxApp { bool OnInit(); MyFrame* frame; public: }; RenderTimer::RenderTimer(BasicDrawPane* pane) : wxTimer() { RenderTimer::pane = pane; } void RenderTimer::Notify() { pane->Refresh(); } void RenderTimer::start() { wxTimer::Start(10); } IMPLEMENT_APP(MyApp) class MyFrame : public wxFrame { RenderTimer* timer; BasicDrawPane* drawPane; public: MyFrame() : wxFrame((wxFrame *)NULL, -1, wxT("Hello wxDC"), wxPoint(50,50), wxSize(400,200)) { wxBoxSizer* sizer = new wxBoxSizer(wxHORIZONTAL); drawPane = new BasicDrawPane( this ); sizer->Add(drawPane, 1, wxEXPAND); SetSizer(sizer); timer = new RenderTimer(drawPane); Show(); timer->start(); } ~MyFrame() { delete timer; } void onClose(wxCloseEvent& evt) { timer->Stop(); evt.Skip(); } DECLARE_EVENT_TABLE() }; BEGIN_EVENT_TABLE(MyFrame, wxFrame) EVT_CLOSE(MyFrame::onClose) END_EVENT_TABLE() bool MyApp::OnInit() { frame = new MyFrame(); frame->Show(); return true; } BEGIN_EVENT_TABLE(BasicDrawPane, wxPanel) EVT_PAINT(BasicDrawPane::paintEvent) END_EVENT_TABLE() BasicDrawPane::BasicDrawPane(wxFrame* parent) : wxPanel(parent) { } void BasicDrawPane::paintEvent(wxPaintEvent& evt) { wxPaintDC dc(this); render(dc); } void BasicDrawPane::paintNow() { wxClientDC dc(this); render(dc); } void BasicDrawPane::render( wxDC& dc ) { static int y = 0; static int y_speed = 2; y += y_speed; if(y<0) y_speed = 2; if(y>200) y_speed = -2; dc.SetBackground( *wxWHITE_BRUSH ); dc.Clear(); dc.DrawText(wxT("Testing"), 40, y); }
Note: When using this method with wxMSW it is very important to include
wxPaintDC dc(this);
in your EVT_PAINT handler. If you do not, your timer (somewhat mysteriously) will not work. Your code may work on other platforms (e.g. wxMAC) but will not work in wxMSW.