CODEpendent

wx-sdl: A tutorial on combining wxWidgets with SDL
Part 1: SDL Initialization and Video Basics

wx-sdl.cc: video tutorial

This is part 1 of a multi-part tutorial. I don't know how many parts there will be. This tutorial covers using an SDL_Surface with wxWidgets to do graphics with SDL (no OpenGL). If you are interested in other concepts besides video, see the index page.

Time to build the example. I'm going to add it class by class, but you can download the full source along with windows and linux binaries below. The source includes a Makefile and a bourne-shell script that will compile it. They can be used in mingw32/msys or unix. I don't know how to build it with Visual Studio (or anything else for that matter). The source file in the archive also has better comments and is more formatted than the code I'll put on the tutorial (to save space). If you get stuck adding one of the classes, you can always take a look at the one in the source archive.

Download [ Source Code | Win32 Binary | Linux Binary ]

Windows XP Screenshot
wx-sdl screenshot

Open your favorite editor (I like jEdit) and create a new file named wx_sdl.cc. You can name it whatever you want, but this is what I used and my Makefile only works with it. cc is probably not the most common cpp file extention, but it's the one I use. You could change it to C or cpp or cxx or whatever you like if you don't like cc. Add the following to the file.

#include <iostream>

#include <wx/wxprec.h>

#ifndef WX_PRECOMP
    #include <wx/wx.h>
#endif

#include <wx/dcbuffer.h>
#include <wx/image.h>

#include "SDL.h"

class SDLApp : public wxApp {
    DECLARE_CLASS(SDLApp)
    
private:
    SDLFrame *frame;
    
public:
    bool OnInit();
    int OnRun();
    int OnExit();
};

bool SDLApp::OnInit() {
    // create the SDLFrame
    frame = new SDLFrame;
    frame->SetClientSize(640, 480);
    frame->Centre();
    frame->Show();
    
    // Our SDLFrame is the Top Window
    SetTopWindow(frame);

    // initialization should always succeed
    return true;
}

int SDLApp::OnRun() {
    // initialize SDL
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        std::cerr << "unable to init SDL: " << SDL_GetError() << '\n';
        
        return -1;
    }
    
    // generate an initial idle event to start things
    wxIdleEvent event;
    event.SetEventObject(&frame->getPanel());
    frame->getPanel().AddPendingEvent(event);

    // start the main loop
    return wxApp::OnRun();
}

int SDLApp::OnExit() {
    // cleanup SDL
    SDL_Quit();
    
    // return the standard exit code
    return wxApp::OnExit();
}

IMPLEMENT_CLASS(SDLApp, wxApp)
IMPLEMENT_APP(SDLApp)

Mostly basic setup stuff. Header file includes for wxWidgets, SDL, and the STL iostream library. We will need wx/image.h and wx/dcbuffer.h later when we have our other classes, so we'll go ahead and add them now.

I expect people reading this to have at least a passing familiarity with wxWidgets and SDL. I am using wxWidgets 2.6.1 and SDL 1.2.8. I won't be explaining every single concept, but it shouldn't be too hard to follow if you have some familiarity with wxWidgets and SDL.

There are three methods in our SDLApp class: OnInit, OnRun, and OnExit. They are called as you might expect, on initialization of the app, when the app is ready to run, and when the app is exiting. We'll start with the OnInit() method.

    // create the SDLFrame
    frame = new SDLFrame;
    frame->SetClientSize(640, 480);
    frame->Centre();
    frame->Show();
    
    // Our SDLFrame is the Top Window
    SetTopWindow(frame);

Here we create the SDLFrame and make it the top window. Don't worry about what an SDLFrame is yet, we haven't built it yet. For now it's enough to know it extends a wxFrame. We set the client size (which will later be occupied by the window we are drawing on) to 640x480, center the frame on the screen, and make it visible.

The OnInit() method is very simple, so we can move on to the OnRun() method now.

    // initialize SDL
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        std::cerr << "unable to init SDL: " << SDL_GetError() << '\n';
        
        return -1;
    }

Here we initialize the SDL library. If we can't, we don't continue. The only component we are using here is video, so it's the only part of SDL we need to initialize.

    // generate an initial idle event to start things
    wxIdleEvent event;
    event.SetEventObject(&frame->getPanel());
    frame->getPanel().AddPendingEvent(event);

This step is a little confusing since we don't have the SDLFrame or SDLPanel yet, but it will become clear once we do. All our drawing happens during idle events, so we need to generate one to start things.

    // start the main loop
    return wxApp::OnRun();

Finally, we call our parent wxApp::OnRun() method which starts the wxWidgets main event loop processing. This will allow wxWidgets to handle events normally as if we were not using SDL at all. This behavior will not work if you need to catch SDL and wxWidgets events. But since we don't need to do that in this program, the default behavior will suffice. Finally, the OnExit() method.

    // cleanup SDL
    SDL_Quit();
    
    // return the standard exit code
    return wxApp::OnExit();

The OnExit() method is even simpler than the OnInit() method. Most SDL programs use the atexit() function to register the SDL_Quit() callback, but we will put it here. You could use atexit if you like, but it seems less wxWidgets-y to me.

The last two lines just implement the rest the wxApp extended SDLApp class and generate a portable main method. Now that we have our basic application class, we can start building our SDLFrame.

Add these lines to the wx_sdl.cc file just after the SDL header include.

enum {
    ID_FRAME = 10000,
    ID_PANEL,
    IDM_FILE_EXIT,
    IDM_HELP_ABOUT
};

class SDLFrame : public wxFrame {
    DECLARE_CLASS(SDLFrame)
    DECLARE_EVENT_TABLE()
    
private:
    SDLPanel *panel;

    void onFileExit(wxCommandEvent &event);
    void onHelpAbout(wxCommandEvent &event);
    
public:
    SDLFrame();
    SDLPanel &getPanel();
};

inline void SDLFrame::onFileExit(wxCommandEvent &) { Close(); }
inline SDLPanel &SDLFrame::getPanel() { return *panel; }

IMPLEMENT_CLASS(SDLFrame, wxFrame)

BEGIN_EVENT_TABLE(SDLFrame, wxFrame)
    EVT_MENU(IDM_FILE_EXIT, SDLFrame::onFileExit)
    EVT_MENU(IDM_HELP_ABOUT, SDLFrame::onHelpAbout)
END_EVENT_TABLE()

SDLFrame::SDLFrame() {
    // Create the SDLFrame
    Create(NULL, ID_FRAME, wxT("Frame Title"), wxDefaultPosition,
           wxDefaultSize, wxCAPTION | wxSYSTEM_MENU | 
           wxMINIMIZE_BOX | wxCLOSE_BOX);

    // create the main menubar
    wxMenuBar *mb = new wxMenuBar;
    
    // create the file menu
    wxMenu *fileMenu = new wxMenu;
    fileMenu->Append(IDM_FILE_EXIT, wxT("E&xit"));
    
    // add the file menu to the menu bar
    mb->Append(fileMenu, wxT("&File"));
    
    // create the help menu
    wxMenu *helpMenu = new wxMenu;
    helpMenu->Append(IDM_HELP_ABOUT, wxT("About"));
    
    // add the help menu to the menu bar
    mb->Append(helpMenu, wxT("&Help"));
    
    // add the menu bar to the SDLFrame
    SetMenuBar(mb);
    
    // create the SDLPanel
    panel = new SDLPanel(this);
}

void SDLFrame::onHelpAbout(wxCommandEvent &) {
    wxMessageBox(wxT("wx-sdl tutorial\nCopyright (C) 2005 John Ratliff"),
                 wxT("about wx-sdl tutorial"), wxOK | wxICON_INFORMATION);
}

SDLFrame adds the primary widgets to our application. We get a Frame for our application and a menu to click on. SDLFrame has three methods and a constructor. We also defined an unnamed enumeration for some constants we are going to use. I'll start with the constructor.

    // Create the SDLFrame
    Create(NULL, ID_FRAME, wxT("Frame Title"), wxDefaultPosition,
           wxDefaultSize, wxCAPTION | wxSYSTEM_MENU | 
           wxMINIMIZE_BOX | wxCLOSE_BOX);

We create our wxFrame with some basic options. We are the top window, so we have no parent. We use ID_FRAME for our identifier. Our title is "Frame Title". We set the position and size in SDLApp, so we'll leave the defaults for now. Finally, the style is the default style for wxFrame with the resizeable border and maximize button removed.

    // create the main menubar
    wxMenuBar *mb = new wxMenuBar;
    
    // create the file menu
    wxMenu *fileMenu = new wxMenu;
    fileMenu->Append(IDM_FILE_EXIT, wxT("E&xit"));
    
    // add the file menu to the menu bar
    mb->Append(fileMenu, wxT("&File"));
    
    // create the help menu
    wxMenu *helpMenu = new wxMenu;
    helpMenu->Append(IDM_HELP_ABOUT, wxT("About"));
    
    // add the help menu to the menu bar
    mb->Append(helpMenu, wxT("&Help"));
    
    // add the menu bar to the SDLFrame
    SetMenuBar(mb);

Now we create the menu bar for the SDLFrame. We have two menus, File and Help. The File menu has an Exit item, and the Help menu has an About item. They're both fairly standard.

BEGIN_EVENT_TABLE(SDLFrame, wxFrame)
    EVT_MENU(IDM_FILE_EXIT, SDLFrame::onFileExit)
    EVT_MENU(IDM_HELP_ABOUT, SDLFrame::onHelpAbout)
END_EVENT_TABLE()

To handle events from the menu items, we setup the event table. When exit is selected, we call SDLFrame::onFileExit. When about is selected, we call SDLFrame::onHelpAbout.

    // create the SDLPanel
    panel = new SDLPanel(this);

Finally, we create our sole client window. This is the SDLPanel, which we will use to draw on. Don't worry too much about SDLPanel yet, since we haven't created it yet. For now it is enough to know that is extends a wxPanel.

Our three methods are quite simple, so I won't even repost them here. onFileExit() simply calls Close() to close the frame. onHelpAbout() displays a wxMessageBox with some basic information. getPanel() returns a reference to our SDLPanel.

Now it's time to create our SDLPanel. Add these lines right after the end of the unnamed enumeration.

class SDLPanel : public wxPanel {
    DECLARE_CLASS(SDLPanel)
    DECLARE_EVENT_TABLE()
    
private:
    SDL_Surface *screen;

    void onPaint(wxPaintEvent &event);
    void onEraseBackground(wxEraseEvent &event);
    void onIdle(wxIdleEvent &event);
    void createScreen();
    
public:
    SDLPanel(wxWindow *parent);
    ~SDLPanel();
};

inline void SDLPanel::onEraseBackground(wxEraseEvent &) {}

IMPLEMENT_CLASS(SDLPanel, wxPanel)

BEGIN_EVENT_TABLE(SDLPanel, wxPanel)
    EVT_PAINT(SDLPanel::onPaint)
    EVT_ERASE_BACKGROUND(SDLPanel::onEraseBackground)
    EVT_IDLE(SDLPanel::onIdle)
END_EVENT_TABLE()

SDLPanel::SDLPanel(wxWindow *parent) : wxPanel(parent, ID_PANEL), screen(NULL) {
    // ensure the size of the wxPanel
    wxSize size(640, 480);
    
    SetMinSize(size);
    SetMaxSize(size);
}

SDLPanel::~SDLPanel() {
    if (screen != NULL) {
        SDL_FreeSurface(screen);
    }
}

void SDLPanel::onPaint(wxPaintEvent &) {
    // can't draw if the screen doesn't exist yet
    if (screen == NULL) {
        return;
    }
    
    // lock the surface if necessary
    if (SDL_MUSTLOCK(screen)) {
        if (SDL_LockSurface(screen) < 0) {
            return;
        }
    }
    
    // create a bitmap from our pixel data
    wxBitmap bmp(wxImage(screen->w, screen->h, 
                    static_cast<unsigned char *>(screen->pixels), true));
    
    // unlock the screen
    if (SDL_MUSTLOCK(screen)) {
        SDL_UnlockSurface(screen);
    }
    
    // paint the screen
    wxBufferedPaintDC dc(this, bmp);
}

void SDLPanel::onIdle(wxIdleEvent &) {
    // create the SDL_Surface
    createScreen();
    
    // Lock surface if needed
    if (SDL_MUSTLOCK(screen)) {
        if (SDL_LockSurface(screen) < 0) {
            return;
        }
    }
    
    // Ask SDL for the time in milliseconds
    int tick = SDL_GetTicks();
    
    for (int y = 0; y < 480; y++) {
        for (int x = 0; x < 640; x++) {
            wxUint32 color = (y * y) + (x * x) + tick;
            wxUint8 *pixels = static_cast<wxUint8 *>(screen->pixels) + 
                              (y * screen->pitch) +
                              (x * screen->format->BytesPerPixel);

            #if SDL_BYTEORDER == SDL_BIG_ENDIAN
                pixels[0] = color & 0xFF;
                pixels[1] = (color >> 8) & 0xFF;
                pixels[2] = (color >> 16) & 0xFF;
            #else
                pixels[0] = (color >> 16) & 0xFF;
                pixels[1] = (color >> 8) & 0xFF;
                pixels[2] = color & 0xFF;
            #endif
        }
    }
    
    // Unlock if needed
    if (SDL_MUSTLOCK(screen)) {
        SDL_UnlockSurface(screen);
    }
    
    // refresh the panel
    Refresh(false);
    
    // throttle to keep from flooding the event queue
    wxMilliSleep(33);
}

void SDLPanel::createScreen() {
    if (screen == NULL) {
        int width, height;
        GetSize(&width, &height);
        
        screen = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 
                                      24, 0, 0, 0, 0);     
    }
}

This class is the largest and most daunting, but is not really very difficult if you break it down piece by piece. There is a constructor, a destructor, and four methods. We'll start with the constructor.

SDLPanel::SDLPanel(wxWindow *parent) : wxPanel(parent, ID_PANEL), screen(NULL) {
    // ensure the size of the wxPanel
    wxSize size(640, 480);
    
    SetMinSize(size);
    SetMaxSize(size);
}

The constructor is fairly simple. First we make call our parent constructor wxPanel making parent our parent and ID_PANEL our identifier. Remember in SDLFrame when we created the SDLPanel, we passed this as the parent, so SDLFrame is the parent of SDLPanel.

Finally, to ensure we have the right size window, we set the SDLPanel's minimum and maximum size. SDL_Surface requires multiples of 4 in the width and height, so if this is off by even a pixel or two, SDL will crash, or worse. It might be better to use sizers here, but I wanted to keep it simple.

Remember back in SDLApp when we generated an idle event and sent it to the SDLPanel? Now we'll see why we did that. First, let's take a look at the event table for the SDLPanel.

BEGIN_EVENT_TABLE(SDLPanel, wxPanel)
    EVT_PAINT(SDLPanel::onPaint)
    EVT_ERASE_BACKGROUND(SDLPanel::onEraseBackground)
    EVT_IDLE(SDLPanel::onIdle)
END_EVENT_TABLE()

A pretty straightforward table. Paint events are handled by onPaint(). Erase background events are handled by onEraseBackground(). Finally, idle events are handled by onIdle. So let's take a look at onIdle().

    // create the SDL_Surface
    createScreen();

The first part simply tells us to create the screen. The screen is used to draw offscreen. Later, we'll blit it to our SDLPanel when we paint. Let's take a look at createScreen() so we can see what the screen is.

    if (screen == NULL) {
        int width, height;
        GetSize(&width, &height);
        
        screen = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 
                                      24, 0, 0, 0, 0);     
    }

So, if our screen is NULL (we haven't made one yet), we create one using SDL_CreateRGBSurface. The important parameters are defined. SDL_SWSURFACE is used to keep the buffer in memory. width and height come from our SDLPanel. 24 is the surface depth (24-bit RGB color). The other parameters are given 0 for default values, but they are not used anyways.

Now that we have a screen and know what it is, let's go back to onIdle().

    // Lock surface if needed
    if (SDL_MUSTLOCK(screen)) {
        if (SDL_LockSurface(screen) < 0) {
            return;
        }
    }

Here we make sure to lock the SDL_Surface before using it, if necessary. Next comes the good part, actually creating some kind of image.

    // Ask SDL for the time in milliseconds
    int tick = SDL_GetTicks();
    
    for (int y = 0; y < 480; y++) {
        for (int x = 0; x < 640; x++) {
            wxUint32 color = (y * y) + (x * x) + tick;
            wxUint8 *pixels = static_cast<wxUint8 *>(screen->pixels) + 
                              (y * screen->pitch) +
                              (x * screen->format->BytesPerPixel);

            #if SDL_BYTEORDER == SDL_BIG_ENDIAN
                pixels[0] = color & 0xFF;
                pixels[1] = (color >> 8) & 0xFF;
                pixels[2] = (color >> 16) & 0xFF;
            #else
                pixels[0] = (color >> 16) & 0xFF;
                pixels[1] = (color >> 8) & 0xFF;
                pixels[2] = color & 0xFF;
            #endif
        }
    }

Worry not; it's less complicated than it looks. For each pixel, we generate a color based on the square of the distance from the origin (equation of a circle) and the current time. This will make many cool circles all over the display.

The pixels pointer is basically a pointer to the position in the screen data where the pixel we want to modify is. We're using 24-bit color, so we have three 8-bit values. When we modify them though, we need to account for byte order, otherwise the colors will be different on machines with different endian-ness. If you don't know what endian-ness is, don't worry about it. Just know we have to do things one way for big endian machines (like Solaris Sparc and Mac PPC), and the opposite for little endian machines (like Windows x86 and Atari 6502).

Thanks go to Jari Komppa, who this pixel generating code is borrowed from. Visit his SDL tutorial website for more information and the original source. I have modified it slightly to work in 24-bit depth mode.

    // Unlock if needed
    if (SDL_MUSTLOCK(screen)) {
        SDL_UnlockSurface(screen);
    }
    
    // refresh the panel
    Refresh(false);
    
    // throttle to keep from flooding the event queue
    wxMilliSleep(33);

After we are done manipulating the pixels, we unlock the surface. Then we call Refresh() which will generate a paint event. Finally, since idle time occurs so often, we need to sleep for a while to make sure we're not flooding the event queue with refresh events. This is clearly not a very precise timing mechanism, and this will be affected by the type of machine running the program.

onIdle() generated a paint event, so now onPaint() will get called. So let's take a look at onPaint().

    // can't draw if the screen doesn't exist yet
    if (screen == NULL) {
        return;
    }
    
    // lock the surface if necessary
    if (SDL_MUSTLOCK(screen)) {
        if (SDL_LockSurface(screen) < 0) {
            return;
        }
    }
    
    // create a bitmap from our pixel data
    wxBitmap bmp(wxImage(screen->w, screen->h, 
                    static_cast<unsigned char *>(screen->pixels), true));
    
    // unlock the screen
    if (SDL_MUSTLOCK(screen)) {
        SDL_UnlockSurface(screen);
    }

A modicum of error checking. First make sure we have a screen, because we can't paint the panel using a screen that doesn't exist. Then we have to lock the screen before we can access it.

The next part is the most interesting though. We build a wxBitmap from a wxImage we create from the SDL pixel data. This will give us a platform dependent bitmap we can blit to the screen. After we have built this wxBitmap, we can unlock the screen.

    // paint the screen
    wxBufferedPaintDC dc(this, bmp);

Finally, we blit the bmp to the screen using a wxBufferedPaintDC. This helps us avoid flicker. We could have used a regular wxPaintDC and just blitted the bitmap, but the code is easier if we use the wxBufferedPaintDC with the bitmap we already have.

Lastly, because paint and erase background events can occur independently of our event generation, we need to handle them. Paint events are already taken care of, so that just leaves erase background events. To avoid flicker, we ignore the erase background event. The handler is an empty body.

Finally, we'll take a look at the destructor.

    if (screen != NULL) {
        SDL_FreeSurface(screen);
    }

The destructor just takes care of deallocating the SDL_Surface.

Conclusion

I hope this tutorial has been useful. If you have any questions, feel free to contact me if you have questions.

Thanks

I would like to thank the following people for their contributions to this tutorial.

 


Need to contact us? We can be reached by email or via our online feedback form.


Copyright © 2005 CODEpendent
All Rights Reserved

Get Firefox!    Valid HTML 4.01!    Made with jEdit