How to use buttons / menus / etc. with SDL?
There are 2 main solutions:
Use a GUI toolkit implement in SDL. That's the most simple solution. There are several GUI libraries written for SDL available from the libraries page at http://www.libsdl.org/libraries.php . This thread (1) mentions Guichan (C++) and Agar (C).
- Embbed the SDL screen in a general-purpose GUI toolkit such as GTK+. This can be useful in some cases, for example for a game editor. SDL wasn't meant for this, but there are a few work-arounds.
The following describe several solutions for embeddeding the SDL window in your application (point #2).
Manually copy from non-window SDL_Surface to the GUI toolkit
You manipulate a SDL_Surface as usual, but you manually copy it as a pixel array to your tookit widget. This is the only technique that allow you to use SDL in several windows.
There is a nice demo for wxWidgets at http://code.technoplaza.net/wx-sdl/part1/ . The demo has some limitations:
The code specifies no depth:
wxBitmap bmp(wxImage(screen->w, screen->h, static_cast<unsigned char *>(screen->pixels), true));
This works because the code in wxImage and wxBitmap hard-codes a 24bit depth with RGB byte order. This implies you need to use that depth spec; it won't work with another depth for your target SDL_Surface. You'll need to fix this in the demo code when creating the target SDL_Surface, otherwise colors may get swapped (blue<->red, etc.)
Uint32 rmask, gmask, bmask, amask; #if SDL_BYTEORDER == SDL_BIG_ENDIAN rmask = 0xff000000; gmask = 0x00ff0000; bmask = 0x0000ff00; amask = 0x00000000; #else rmask = 0x000000ff; gmask = 0x0000ff00; bmask = 0x00ff0000; amask = 0x00000000; #endif screen = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 24, rmask, gmask, bmask, amask);
With Gtk+, there is a similar solution where you use gdk_draw_rgb_image on SDL_Surface->pixels. Similarly, it involves using (or converting to) a 24bits-RGB format with precise masks, usually not the same as the physical screen, so there will be one additional blit of the pixel data for conversion. Note that it only supports blitting the whole surface:
GdkDrawable* drawable = ...; /* eg. widget->window */ /* A GC is necessary */ GdkGC* ignored_gc = gdk_gc_new(drawable); int width = -1, height = -1; gdk_drawable_get_size(drawable, &width, &height); /* gdk_draw_rgb_image uses hardcoded 24bit RGB (in that order), which is usually the reverse order than my x86 display */ Uint32 rmask, gmask, bmask, amask; #if SDL_BYTEORDER == SDL_BIG_ENDIAN rmask = 0xff000000; gmask = 0x00ff0000; bmask = 0x0000ff00; #else rmask = 0x000000ff; gmask = 0x0000ff00; bmask = 0x00ff0000; #endif amask = 0x00000000; SDL_Surface *screen = SDL_CreateRGBSurface(SDL_SWSURFACE, width, height, 24, rmask, gmask, bmask, amask); SDL_BlitSurface(...., screen, NULL); ... /* ~Flip */ gdk_draw_rgb_image(drawable, ignored_gc, 0, 0, screen->w, screen->h, GDK_RGB_DITHER_NONE, (guchar*)screen->pixels, screen->pitch); /* Clean-up when quitting game: */ SDL_FreeSurface(screen); g_object_unref(ignored_gc);
If you're ok with using other pixel formats, one more efficient way to draw in a Gdk window is to create a SDL_Surface from a GdkImage (this way you skip a pixel conversion from 24bit-RGB before each blit). It also supports partial blits. You'll use gdk_draw_image:
GdkDrawable* drawable = ...; /* eg. widget->window */ /* A GC is necessary */ GdkGC* ignored_gc = gdk_gc_new(drawable); int width = -1, height = -1; gdk_drawable_get_size(drawable, &width, &height); GdkVisual* visual = gdk_drawable_get_visual(drawable); GdkImage* image = gdk_image_new(GDK_IMAGE_FASTEST, visual, width, height); SDL_Surface* screen = SDL_CreateRGBSurfaceFrom(image->mem, width, height, image->bits_per_pixel, image->bpl, visual->red_mask, visual->green_mask, visual->blue_mask, 0); SDL_BlitSurface(...., screen, NULL); ... /* ~Flip/UpdateRect */ gdk_draw_image(drawable, ignored_gc, image, 0, 0, 0, 0, -1, -1); ... /* Clean-up when quitting game: */ SDL_FreeSurface(screen); g_object_unref(image); g_object_unref(ignored_gc);
The mouse or keyboard events are caught by the GUI toolkit only, not by SDL (at least under X11).
The SDL_WINDOWID hack
This makes SDL use an existing window instead of creating a new one. This works under X11 and Win32. Basically you putenv("SDL_WINDOWID=XXX") where XXX is an existing window's ID. You can only use it on one existing window (not several).
Code can be found for various toolkits:
- Gtk+ (v1):
draw on the window: http://www.libsdl.org/projects/gtk-demo/
new widget: http://gtksdl.sourceforge.net/
Compiler fixes: http://gtk.developpez.com/faq/?page=gtksdl (french)
Win32 issues: http://www.developpez.net/forums/showthread.php?t=202660 (french)
Another version: http://gtksdl.developpez.com/ (french)
http://gens.consolemul.com/ Gens uses this technique manually (without an encapsulated widget)
GtkPygame: complete widget (WIP, feedback welcome): http://git.savannah.gnu.org/gitweb/?p=freedink/windinkedit.git;a=blob;f=python/gtkpygame.py;hb=HEAD
Proof-of-concept: http://sparcs.kaist.ac.kr/~tinuviel/devel/gtksdl.py (does not handle expose issues 1 2)
Similar code that disable DirectX: http://osdir.com/ml/python.pygame/2003-01/msg00107.html (if sys.platform.startswith('win'): os.environ['SDL_VIDEODRIVER'] = 'windib')
gtmmm + Gtk plug/socket: http://email@example.com/msg08601.html
You do not receive the usual SDL events for this solution. With C, SDL still catches SIGTERM and converts it to a SDL_QUIT event; Python however catches SIGTERM first. The ticks also work.
Using Gtk socket is mentioned in the SDL_WINDOWID section, because you can put SDL in a socket by passing the socket id to SDL_WINDOWID.
The other way around, does not involved SDL_WINDOWID, somewhat works too: passing the SDL window ID (pygame.display.get_wm_info().get("window")) to a Gtk socket (socket.add_id()). Since you "steal" SDL out of an existing window, you get a now-empty original SDL window lying around.
1 implies input can work with the Gtk socket technique. According to my tests, the window events are handled by the original empty window, and the mouse / keyboard events by the stolen graphics display in the socket window.
Use MDI instead of SDI
That is, Multiple Document Interface instead of Single Document Interface (think Gimp vs. Photoshop).
You initialize SDL as usual, and call your favorite toolkit to create toolbars around the main SDL window.
See the double loop section for examples and limitations.
Double event loop issue
When combining SDL and a GUI, and where your technique supports SDL events, there will be two event loops. Both need to run at the same time!
- Grab one toolkit event at a time, instead of starting the main "magic" tookit event loop.
This Tcl/Tk + SDL hack written by Kent Mein use the MDI technique: http://www.libsdl.org/projects/tcl-demo/ . It grabs one Tk event (Tk_DoOneEvent) in an otherwise classic SDL_PollEvent loop. Your toolkit has to support such a fine-grained rewrite of the main event loop.
http://gens.consolemul.com/ Gens uses while (gtk_events_pending ()) gtk_main_iteration_do(FALSE); and while (gtk_events_pending ()) gtk_main_iteration (); (TODO: difference?)
- Poll the SDL events when inside toolkit event loop:
wxWidgets: the example from technoplaza displays the updated SDL surface during wxWindow's OnIdle: you can easily add a PollEvent() there.
Gtk2: there's a similar function called g_idle_add
- Set an arbitrary timer (e.g. 70ms) with your toolkit (QTimer, g_timeout_add...) and check SDL events in the timer callback.
You'll need to read the events if you want SDL to refresh the window when it's repainted (hidden behind another window, etc.).
putenv or SDL_putenv?
Apparently SDL_putenv takes care of tricky situations where the environment is not the same in the application and in the library: http://lists.libsdl.org/htdig.cgi/sdl-libsdl.org/2005-September/051604.html
Not tested though.