The first tutorial covered using SDL_Surfaces. This tutorial focuses on Joystick input. To keep things simple, we won't use an SDL_Surface for drawing as our only goal here is to illustrate using the SDL_Joystick within a wxWidgets program.
In this program, we are going to create a simple drawing program. It will use the joystick to move the 'trigger', and any button pressed on the joystick will draw a circle at the position of the trigger.
Once again, I'll build class-by-class, but you can download source code and binaries just below.
Download: [ Source Code | Win32 Binary | Linux Binary ]
Windows XP Screenshot
Open your favorite editor and create a new file wx-sdl_joy.cc. Just like before, you can change the name and the extention if you like. This is just what I've chosen, and it matches the source code archive. Add the following code to the file.
#include <cstdio> #include <cstdlib> #include <iostream> #include <stdexcept> #include <vector> #include <wx/wxprec.h> #ifndef WX_PRECOMP #include <wx/wx.h> #endif #include <wx/dcbuffer.h> #include "SDL.h" class SDLApp : public wxApp { DECLARE_CLASS(SDLApp) public: bool OnInit(); int OnExit(); }; bool SDLApp::OnInit() { // Windows doesn't open a console for GUI apps // so redirect stdout/stderr to file to check for error output #ifdef __WXMSW__ std::freopen("stdout.txt", "w", stdout); std::freopen("stderr.txt", "w", stderr); #endif // initialize SDL if (SDL_Init(SDL_INIT_JOYSTICK) < 0) { std::cerr << "unable to init SDL: " << SDL_GetError() << '\n'; return false; } // make sure SDL_Quit is called on exit std::atexit(SDL_Quit); if (SDL_NumJoysticks() == 0) { std::cerr << "no joysticks found\n"; return false; } // create the SDLFrame SDLFrame *frame; try { frame = new SDLFrame; } catch (std::runtime_error &exception) { // we weren't able to open the Joystick std::cerr << exception.what() << '\n'; return false; } frame->SetClientSize(640, 480); frame->Centre(); frame->Show(); // Our SDLFrame is the Top Window SetTopWindow(frame); // if we made it here, we are successfully initialized return true; } int SDLApp::OnExit() { // if no errors occurred, delete our Win32 redirect files #ifdef __WXMSW__ std::fclose(stdout); std::fclose(stderr); wxRemoveFile("stdout.txt"); wxRemoveFile("stderr.txt"); #endif return wxApp::OnExit(); } IMPLEMENT_CLASS(SDLApp, wxApp) IMPLEMENT_APP(SDLApp)Just like in the first tutorial, we're going to start with the SDLApp class. It's similar to the first one, but there are some important differences.
Note the absense of the OnRun() method. We don't need to do any idle event generating here, so we don't need an OnRun() method here. Also note that we initialize SDL before we create our SDLFrame this time. This has some important consequences. Let's take a look at the OnInit() method.
// Windows doesn't open a console for GUI apps // so redirect stdout/stderr to file to check for error output #ifdef __WXMSW__ std::freopen("stdout.txt", "w", stdout); std::freopen("stderr.txt", "w", stderr); #endifThis is something new, although it would have been a good idea to use it in the last tutorial also. (Since I never got any runtime errors while making it, it never occured to me) Windows GUI programs don't open a console, so anything printed to the standard ouput or error streams are never displayed, even if you launch from a terminal like msys or command.com. To overcome this, we'll redirect the stdout and stderr streams to a file on Windows so we can see what the errors are if we get any. This is probably a good argument for log files rather than std stream output for errors, but not for so simple a program as ours.
// initialize SDL if (SDL_Init(SDL_INIT_JOYSTICK) < 0) { std::cerr << "unable to init SDL: " << SDL_GetError() << '\n'; return false; } // make sure SDL_Quit is called on exit std::atexit(SDL_Quit); if (SDL_NumJoysticks() == 0) { std::cerr << "no joysticks found\n"; return false; }Next we initialize the SDL library. Notice we're not using SDL_INIT_VIDEO because we're not doing any video with SDL. We're just going to use the joystick support.
The second thing of note is that we're going to use atexit to register our SDL_Quit function rather than put it in OnExit(). This is important, because if OnInit() returns false, OnExit() doesn't get called. In the last example, we initialized SDL in the OnRun() method, so OnExit() would always be called if SDL were initialized. Therefore, it was safe to put SDL_Quit in the OnExit() method in the last program, but it's not safe here.
Finally, we check to make sure there is a joystick attached to the computer. We can't really do a joystick example if there are no joysticks on this computer. If you don't have one, you can't use this program.
// create the SDLFrame SDLFrame *frame; try { frame = new SDLFrame; } catch (std::runtime_error &exception) { // we weren't able to open the Joystick std::cerr << exception.what() << '\n'; return false; } frame->SetClientSize(640, 480); frame->Centre(); frame->Show(); // Our SDLFrame is the Top Window SetTopWindow(frame);Some minor differences in creating the SDLFrame from our first example. First, we're going to catch exceptions, because SDLPanel might throw an exception in the constructor, which we'll see later. Also, since we no longer need access to the SDLFrame outside of the OnInit() method, it is no longer a member variable, but rather a local. Now let's look at the OnExit() method.
// if no errors occurred, delete our Win32 redirect files #ifdef __WXMSW__ std::fclose(stdout); std::fclose(stderr); wxRemoveFile("stdout.txt"); wxRemoveFile("stderr.txt"); #endifSince OnExit() is not called if OnInit() returns false, we can be sure that if we get to OnExit(), no errors occured during initialization. Therefore, we can delete our empty redirect files. If you use the standard streams elsewhere though, you'll not want to do this. We only use stdout/stderr in the OnInit() method though, so it's safe for us to remove the files here in OnExit().
Time to add another class. Put the following code after the SDL.h header include.
#define ANALOG_THRESHOLD 20000 enum { ID_FRAME = 10000, ID_PANEL, IDM_EDIT_CLEAR }; class SDLFrame : public wxFrame { DECLARE_CLASS(SDLFrame) DECLARE_EVENT_TABLE() private: SDLPanel *panel; void onFileExit(wxCommandEvent &event); void onEditClear(wxCommandEvent &event); void onHelpAbout(wxCommandEvent &event); public: SDLFrame(); }; inline void SDLFrame::onFileExit(wxCommandEvent &) { Close(); } inline void SDLFrame::onEditClear(wxCommandEvent &) { panel->clear(); } IMPLEMENT_CLASS(SDLFrame, wxFrame) BEGIN_EVENT_TABLE(SDLFrame, wxFrame) EVT_MENU(wxID_EXIT, SDLFrame::onFileExit) EVT_MENU(IDM_EDIT_CLEAR, SDLFrame::onEditClear) EVT_MENU(wxID_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(wxID_EXIT, wxT("E&xit")); // add the file menu to the menu bar mb->Append(fileMenu, wxT("&File")); // create the edit menu wxMenu *editMenu = new wxMenu; editMenu->Append(IDM_EDIT_CLEAR, wxT("&Clear")); // add the edit menu to the menu bar mb->Append(editMenu, wxT("&Edit")); // create the help menu wxMenu *helpMenu = new wxMenu; helpMenu->Append(wxID_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 &) { // display standard 'about' box wxMessageBox(wxT("wx-sdl tutorial\nCopyright (C) 2005 CODEpendent"), wxT("about wx-sdl tutorial"), wxOK | wxICON_INFORMATION); }This class is nearly identical to the last one, so I'll just point out the differences quickly.
This class uses wxID_EXIT and wxID_ABOUT for the menu's exit and about identifiers, respectively. The last program should also have done this, but I forgot. The reason for this is so the Mac menus will look mac native. It won't affect any other platform, so it's just a special hint for wxMac.
There is a third menu, the edit menu. This is used to send clear messages to the panel. We'll see what that's about later.
The method to retrieve the SDLPanel has been removed. It is not needed in this program.
These are the only differences. Very minor as I said. So let's continue with the final class, SDLPanel. Add this code just after the unnamed enumeration.
class SDLPanel : public wxPanel { DECLARE_CLASS(SDLPanel) DECLARE_EVENT_TABLE() private: std::vector<wxPoint> points; wxPoint position; SDL_Joystick *joystick; void onPaint(wxPaintEvent &event); void onEraseBackground(wxEraseEvent &event); void onIdle(wxIdleEvent &event); public: SDLPanel(wxWindow *parent); ~SDLPanel(); void clear(); }; 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), position(320, 240) { // open the first Joystick if ((joystick = SDL_JoystickOpen(0)) == NULL) { throw std::runtime_error("unable to open joystick"); } } SDLPanel::~SDLPanel() { // close the joystick SDL_JoystickClose(joystick); } void SDLPanel::onPaint(wxPaintEvent &) { // create the screen buffer wxBufferedPaintDC dc(this); // erase the screen dc.SetBackground(*wxWHITE_BRUSH); dc.Clear(); // set the drawing colors dc.SetPen(*wxBLACK_PEN); dc.SetBrush(*wxBLACK_BRUSH); // draw the point circles for (unsigned int i = 0; i < points.size(); i++) { dc.DrawCircle(points[i], 10); } // draw the trigger circle dc.SetPen(wxPen(wxColour(0, 0, 0xFF))); dc.SetBrush(*wxBLUE_BRUSH); dc.DrawCircle(position, 10); } void SDLPanel::onIdle(wxIdleEvent &event) { wxPoint old = position; bool invalid = false; // poll the joystick SDL_JoystickUpdate(); // get the analog position wxInt16 x = SDL_JoystickGetAxis(joystick, 0); wxInt16 y = SDL_JoystickGetAxis(joystick, 1); // get the number of buttons on the joystick int buttons = SDL_JoystickNumButtons(joystick); // if any button is pressed, add a point circle for (int i = 0; i < buttons; i++) { if (SDL_JoystickGetButton(joystick, i)) { points.push_back(position); invalid = true; break; } } // check if the trigger has been moved if (x > ANALOG_THRESHOLD) { ++position.x; } if (x < -ANALOG_THRESHOLD) { --position.x; } if (y > ANALOG_THRESHOLD) { ++position.y; } if (y < -ANALOG_THRESHOLD) { --position.y; } // if we moved the trigger, we need to update the screen if ((old.x != position.x) || (old.y != position.y)) { invalid = true; } // if the screen is invalid if (invalid) { // refresh Refresh(false); } else { // otherwise, come back for more input processing event.RequestMore(); } } void SDLPanel::clear() { // clear the points vector points.clear(); // refresh the screen Refresh(false); }Once again, SDLPanel is the most complicated class. But it only looks daunting. In fact, it's almost as simple as the last program. We'll start with the constructor.
// open the first Joystick if ((joystick = SDL_JoystickOpen(0)) == NULL) { throw std::runtime_error("unable to open joystick"); }The constructor opens the first joystick SDL can find. If it can't open this joystick, we throw a runtime_error. It would be a better design if we threw a custom exception derived from runtime_error, but for a simple example, I let it go.
Unlike the previous example, we do not need to guarantee any particular size here since we are not using an SDL_Surface. Because of that, we'll just accept whatever SDLFrame::SetClientSize() gives us in the SDLApp::OnInit() method. Let's continue with the onPaint() method.
// create the screen buffer wxBufferedPaintDC dc(this); // erase the screen dc.SetBackground(*wxWHITE_BRUSH); dc.Clear(); // set the drawing colors dc.SetPen(*wxBLACK_PEN); dc.SetBrush(*wxBLACK_BRUSH); // draw the point circles for (unsigned int i = 0; i < points.size(); i++) { dc.DrawCircle(points[i], 10); } // draw the trigger circle dc.SetPen(wxPen(wxColour(0, 0, 0xFF))); dc.SetBrush(*wxBLUE_BRUSH); dc.DrawCircle(position, 10);This is the onPaint() method. It is rather simple. We keep a vector of wxPoint objects in the SDLPanel. For each point in this vector, we draw a filled circle centered at that point with radius 10 in black. We also keep a wxPoint position which is the current location of our trigger. At this location, we draw a circle in blue. If you're curious about why we didn't use wxBLUE_PEN, it's because for some reason, wxWidgets pre-defines a blue brush, but not a blue pen. Simple double-buffered drawing. Let's check out the onIdle() method.
wxPoint old = position; bool invalid = false;We start by defining two variables which will be used to determine if the screen needs to be refreshed.
// poll the joystick SDL_JoystickUpdate(); // get the analog position wxInt16 x = SDL_JoystickGetAxis(joystick, 0); wxInt16 y = SDL_JoystickGetAxis(joystick, 1); // get the number of buttons on the joystick int buttons = SDL_JoystickNumButtons(joystick); // if any button is pressed, add a point circle for (int i = 0; i < buttons; i++) { if (SDL_JoystickGetButton(joystick, i)) { points.push_back(position); invalid = true; break; } } // check if the trigger has been moved if (x > ANALOG_THRESHOLD) { ++position.x; } if (x < -ANALOG_THRESHOLD) { --position.x; } if (y > ANALOG_THRESHOLD) { ++position.y; } if (y < -ANALOG_THRESHOLD) { --position.y; }Next, we read the joystick. If we have any buttons pressed, we add the location of the trigger to our point list. If the joystick has moved, we move the trigger in the direction of the joystick move. Because analog joysticks give values ranging from -32768 to 32767, we use ANALOG_THRESHOLD to determine if the movement is considered a full movement in a direction. This could be modified to allow analog joysticks to move slow or fast depending upon what degree of motion is used. I was too lazy to do that here though, and this isn't really a joystick tutorial so much as a tutorial for using SDL_Joystick with a wxWidgets program.
// if we moved the trigger, we need to update the screen if ((old.x != position.x) || (old.y != position.y)) { invalid = true; } // if the screen is invalid if (invalid) { // refresh Refresh(false); } else { // otherwise, come back for more input processing event.RequestMore(); }Finally, we check to see if we need to repaint the screen. We must repaint either a) if the trigger was moved, or b) if a new circle point was added. We already checked for the circle points, so here we see if our old position differs from our new position.
We could simply always repaint the screen. However, this is inefficient and would waste time that could be used by background processes instead. If you profile your CPU while this is running, you will notice it uses 100% of the CPU. This may seem bad, but it's really not because we are only polling the joystick in idle time. In other words, if any background tasks are running, they will receive the idle time instead. However, if we always repainted the screen, they would not. So, despite the fact that it seems to be using 100% of the CPU, it really only does that when there is nothing else for the CPU to do. We could solve the 100% CPU usage 'problem' by using a timer callback instead of polling in idle time.
If we don't repaint the screen, we need to come back to onIdle() so we can keep polling the joystick. So we tell the idle event to request more idle events.
There is a huge flaw in this program. Because joystick polling is done in idle time, the more circles we have to draw, the slower the trigger will move. This is because every time we redraw the trigger, we have to redraw all the circles, too. If we have to draw a few thousand circles and fill them everytime we move the trigger a single pixel, there will be less and less idle time to poll for joystick events to move the trigger. This could be solved by using a background buffer for the circles and only redrawing the trigger in the onPaint() handler.
There is one more flaw in the program, though much less significant. The trigger can be moved outside the panel. This could be fixed by adding boundary checks to the trigger movement.
Finally, we'll take a look at the clear() method.
// clear the points vector points.clear(); // refresh the screen Refresh(false);The clear() method is very simple. We just clear the points vector and refresh the screen. Now for the destructor.
// close the joystick SDL_JoystickClose(joystick);All the destructor needs to do is close our SDL_Joystick. I've ignored the onEraseBackground() method because it's identical to the one in the last tutorial.
That's basically all there is to using SDL Joystick input in a wxWidgets program. I'll leave you with some ideas for exercises.
- Replace onIdle() joystick polling with a timer callback.
- Replace the onPaint() redraw-all with a background buffer to keep the circles so the joystick doesn't lose speed.
- Customize the program to use multiple colors. Allow selection of colors either by menu, joystick, or both.
- Add a dialog to configure the joystick for this program. Allow the user to select which buttons draw a circle, which change colors, and maybe buttons for moving the axis (for d-pad buttons which are not implemented as digital axis).
- Change the joystick handling to allow the analog axis to move the trigger at a variable speed depending upon the degree to which the axis is moved.
- Add boundary checks for the trigger movement to keep the trigger from leaving the panel area.
Need to contact us? We can be reached by email or via our online feedback form.