Search code examples
c++wxwidgets

wxWidgets best control for drawing realtime graphics


I made a little App using SDL to show some sprite animations, and it works really fine. It's important that the rendering is as smooth as possible.

Since now I need some GUI, I decided to use wxWidgets 3 to create kind of the same application but where I can now also add some GUI elements like menus, modals, etc.

After diving into the wxWidget's wiki, I found there are different ways to draw into a window.

For example:

  • wxClientDC
  • wxPaintDC
  • wxGLCanvas

I also found other kind of classes like the "Graphics Context classes" which I'm still not sure if they could be useful for what I need.

My application already uses an internal buffer which just needs to be copied.

So my question is What's the most appropriate way to do this on wxWidgets? And, how much performance/overhead does that solution impose?

What would be the difference in performance/overhead between the different methods wxWidgets has for drawing pixels inside a window? Why one uses OnPaint events while others don't.


Solution

  • Here's one way to do this:

    // For compilers that support precompilation, includes "wx/wx.h".
    #include "wx/wxprec.h"
    
    #ifdef __BORLANDC__
        #pragma hdrstop
    #endif
    
    // for all others, include the necessary headers (this file is usually all you
    // need because it includes almost all "standard" wxWidgets headers)
    #ifndef WX_PRECOMP
        #include "wx/wx.h"
    #endif
    
    #include <wx/timer.h>
    #include <wx/dcclient.h>
    #include <wx/rawbmp.h>
    
    ///////////// Declarations
    
    class MyFrame : public wxFrame
    {
        public:
            MyFrame( wxWindow* parent, int id = wxID_ANY, wxString title = "Demo",
                     wxPoint pos = wxDefaultPosition, wxSize size = wxDefaultSize,
                     int style = wxDEFAULT_FRAME_STYLE|wxTAB_TRAVERSAL );
            ~MyFrame();
    
        private:
            // Event Handlers
            void OnPaint( wxPaintEvent& event );
            void OnTimer(wxTimerEvent& event);
    
            // Helper function
            void RebuildBufferAndRefresh();
    
            // Private data
            wxWindow* m_renderSurface;
            int m_width;
            int m_height;
            wxBitmap m_bitmapBuffer;
            wxTimer m_timer;
            int m_curRGB;
            unsigned char* m_pixelData;
    };
    
    class MyApp : public wxApp
    {
        public:
            virtual bool OnInit() wxOVERRIDE;
    };
    
    wxDECLARE_APP(MyApp);
    
    
    ///////////// Implementation
    
    MyFrame::MyFrame( wxWindow* parent, int id, wxString title, wxPoint pos,
                      wxSize size, int style )
            :wxFrame( parent, id, title, pos, size, style )
    {
        m_width=100;
        m_height=100;
        m_pixelData = new unsigned char[3*m_width*m_height];
    
        m_renderSurface = new wxWindow(this, wxID_ANY, wxDefaultPosition,
                                         wxSize(m_width,m_height));
        m_renderSurface->SetBackgroundStyle(wxBG_STYLE_PAINT);
        m_renderSurface->Bind(wxEVT_PAINT,&MyFrame::OnPaint,this);
    
        wxBoxSizer* bSizer = new wxBoxSizer( wxVERTICAL );
        bSizer->Add( m_renderSurface, 0 );
        this->SetSizer( bSizer );
        Layout();
    
        m_timer.SetOwner(this);
        m_timer.Start(17);
        this->Bind(wxEVT_TIMER,&MyFrame::OnTimer,this);
        m_curRGB=0;
    }
    
    MyFrame::~MyFrame()
    {
        m_timer.Stop();
        delete[] m_pixelData;
    }
    
    void MyFrame::OnPaint( wxPaintEvent& event )
    {
        wxPaintDC dc(m_renderSurface);
        if(m_bitmapBuffer.IsOk())
        {
            dc.DrawBitmap(m_bitmapBuffer,0,0);
        }
    }
    
    void MyFrame::OnTimer(wxTimerEvent& event)
    {
        RebuildBufferAndRefresh();
    }
    
    void MyFrame::RebuildBufferAndRefresh()
    {
        // Build the pixel buffer here, for this simple example just set all
        // pixels to the same value and then increment that value.
        for ( int y = 0; y < m_height; ++y )
        {
            for ( int x = 0; x < m_width; ++x )
            {
                m_pixelData[3*y*m_width+3*x]=m_curRGB;
                m_pixelData[3*y*m_width+3*x+1]=m_curRGB;
                m_pixelData[3*y*m_width+3*x+2]=m_curRGB;
            }
        }
    
        ++m_curRGB;
        if(m_curRGB>255)
        {
            m_curRGB=0;
        }
    
        // Now transfer the pixel data into a wxBitmap
        wxBitmap b(m_width,m_height,24);
        wxNativePixelData data(b);
    
        if ( !data )
        {
            // ... raw access to bitmap data unavailable, do something else ...
            return;
        }
    
        wxNativePixelData::Iterator p(data);
    
        int curPixelDataLoc = 0;
        for ( int y = 0; y < m_height; ++y )
        {
            wxNativePixelData::Iterator rowStart = p;
            for ( int x = 0; x < m_width; ++x, ++p )
            {
                p.Red() = m_pixelData[curPixelDataLoc++];
                p.Green() = m_pixelData[curPixelDataLoc++];
                p.Blue() = m_pixelData[curPixelDataLoc++];
            }
            p = rowStart;
            p.OffsetY(data, 1);
        }
    
        m_bitmapBuffer=b;
        m_renderSurface->Refresh();
        m_renderSurface->Update();
    }
    
    
     bool MyApp::OnInit()
    {
        MyFrame* frame = new MyFrame(NULL);
        frame->Show();
        return true;
    }
    
    wxIMPLEMENT_APP(MyApp);
    

    The basic idea here is copy your buffer into a wxBitmap and then draw that bitmap in the paint handler. The paint handler is then triggered by the calls to Refresh and Update. The reason for this is that system will also call the paint method for various reasons such as the mouse cursor running over the render surface. By doing it this way, there is a bitmap that can be drawn for both your and the system's calls to refresh the surface.

    In the simple example posted above, I'm just using a simple timer to call the RebuildBufferAndRefresh method approximately 60 times a second. Your application will probably have a better way to to determine when a refresh is needed and you can use that way to call RebuildBufferAndRefresh instead.

    Obviously most of the work is done in the RebuildBufferAndRefresh method. That method is broken into 3 parts. The first part builds the internal buffer. The second part copies that buffer into a wxBitmap for the reasons stated above. The third part just calls refresh and update to force the render surface to be redrawn.

    There may be better ways to do this, but I think this is the most straight forward wxWidgets-ey to do constantly render a buffer of pixel data to a window.

    As to how much overhead this approach requires, it's basically a memcopy from your buffer to a bitmap, but the nested loop will be less efficient than a real memcopy. After that the paint handler does nothing but draw the bitmap. That will probably be accomplished by a blit to the system's backbuffer and should be quite fast. I hope that helps.