SDL GUI in C on Windows

This is aimed at anyone interested in writing their own GUI for use with SDL2. I’m assuming you have a decent Windows C compiler. If not you could install the free Microsoft Visual Studio Community 2017 and when it asks what you want it for select C/C++ development.

For installing SDL, I’ve used SDL 2.0 available from the libsdl website. There are some excellent tutorials including installing SDL. You’ll also need SDL2_image and SDL_TTF.

I never understood why these aren’t included with SDL. You certainly can’t manage without loading images! I downloaded these two development libraries and copied the header files into the SDL include folder and the libs into the SDL lib win32 folder. The two extra libs should be added to the project linker input additional dependencies along with SD2.lib and SDL2main.lib. The dlls should all be copied into the debug and release folders created by compiling.
Just as a check you should have libfreetype-6.dll, libjpeg-9.dll, libpng16-16.dll, libtiff-5.dll, libwebp-4.dll, SDL2.dll. SDL2_image.dll, SDL2_ttf.dll, zlib1.dll. If you’re not using tiff or webp images then you can remove those two dlls.
At that point it should all compile and you are ready to start writing C code.

The GUI

SDL does not provide a GUI. What is does provide is event driven mouse and keyboard functions, and functions to blit graphics very rapidly onto a SDL surface. So we have to build a GUI from the ground up. I created one a few years back for a site that has just gone bust, and I’ve recently revamped it for SDL2.
If C were an Object oriented language, we’d create a base class for the control and polymorph it in descendant classes. In C we allocate structs to do this.

The controls we’ll use include a panel, label, a clickable button, a checkbox, a list box with multiple list items and an image. All controls have a ‘base class’, in this case a struct that contains the following:

enum controltype {esdlpanel,esdlbutton,esdlclickabletext,esdlcheckbox,esdllistbox,esdllistitem,
	edslpopup,esdltextentry,esdllabel,esdlimage};

#define	sdlbase enum controltype ctype;\
int x,y,width,height,color,clickable,visible;\
SDL_Color textcolor;\
void (*pRender)(struct sdlcontrol * self);\
void (*pFree)(struct sdlcontrol * self);\
void (*pClick)(struct sdlcontrol * self);\
void (*pPreClick)(struct sdlcontrol * self);\
struct sdlcontrol * nextcontrol

struct sdlcontrol { sdlbase; };
typedef struct sdlcontrol * psdlcontrol;
typedef char * pchar;

Not all of the controls in the controltype enum have been implemented yet.
I’ve defined all the common fields with a macro sdlbase. You can see it has a type, positions x,y and height, width and colours for the background (color) and text colour. There’s a visible flag, set to 1 by default.
There are four function pointers. One for rendering the control, one for freeing its memory, one for responding to a click and one for a pre-click. We’ll come to that later. All four function pointers include a sdlcontrol as a parameter whether it’s for drawing it with render or handling the click.

All controls are structs defined by the sdlbase macro with extra struct members and the odd function pointer added for each control. For example a button looks like this.

struct sdlbutton {
	sdlbase;
	pchar labeltext;
	int isDown;
	int countDown;
};

The sdlbase macro is substituted at compile time and the button adds three extra fields in the struct, a label text field, a flag isDown and a countDown timer.

All controls are added to a panel by first calling this function which returns a pointer to the container panel. It owns all the controls. Think of it as the first item in a linked list.

psdlcontrol initsdlguilib(int x,int y,int width,int height,int color);

There are various ‘add a control’ to the controls list functions. For instance the addlabel function looks like this and returns a pointer to a control.

psdlcontrol addlabel(int x,int y, pchar text,SDL_Color textcolor);

So to build the GUI we call initsdlguilib which defines the container panel and then add all the controls within that container. It’s held in memory with just a pointer to the panel and each control has a next control so the structure is a linked list of sdlcontrols.
If you look through sdlgui.c, you’ll see the ‘add a control’ functions. Here’s addlabel.

psdlcontrol addlabel(int x,int y, pchar text,SDL_Color textcolor) {
	struct sdllabel * label;
	int width,height;
	int textlen = strlen(text) + 1;
	pchar textptr=(pchar)malloc(textlen);
              TTF_SizeText(tfont, text, &width, &height);
	label = (struct sdllabel *)addcontrol(esdllabel,x,y,width,height,sizeof(struct sdllabel),textcolor);
	label->labeltext = textptr;
	label->pClick=NULL;
	strcpy_s(label->labeltext, textlen,text);
	return (psdlcontrol)label;
}

The addcontrol() is a common function that is called to set all the common fields and allocate enough memory for the full control. One of the parameters is the size of the full control e.g. sizeof(struct sdlbutton) for a button. So there’s space for the common control and the extra fields belonging to sdlbutton. Also addcontrols sets a clickable flag to say whether a control can be clicked. Labels usually aren’t clicked for example.

Because it’s SDL, it’s rendering the GUI 60 times each second. The RenderGui function is passed the address of the panel and walks the linked list calling the pRender function pointer for each control.

void RenderGUI(psdlcontrol lib) {
	psdlcontrol control = lib;
	while (control) {
		if (control->pRender) {
		    control->pRender(control); /* render self */
		}
		control = control->nextcontrol;
	}
}

When a control is clicked, say a button, it call the PreClick function first. This is it for a button.

/* Handlers for pre/post click on controls */
void buttonPreclick(psdlcontrol self) {
	struct sdlbutton * pb= (struct sdlbutton *)self;
	pb->isDown = 1;
	pb->countDown = 20;
}

It puts the button in a down position by setting the countDown to 20 and the isDown flag to true (1). For the next 1/3rd of a second (20/60th) the RenderButton function draws the button displaced to show it’s down. After 20 ticks, the isDown flag is cleared and the button springs back up.

Test Program

I added a free true type font GUNPLAY_.ttf to both Debug and Release folders. As it’s using SDL_TTF, we need a decent font and this one provides a military stencil type font.
Note that when debugging in Visual Studio, by default the debug folder is not the Debug folder but the one above it. This is the $ProjectDir. If you change this in Configuration Properties\Debugging\Working Directory to $(OutputPath) then it runs in the Debug folder and that’s where it looks for files such as TTF, pngs etc.
With the support files in the same folder as the exe, I can also run it direct from there. This is how I found a showstopper bug. It only appeared in the Release version. It was the noobie trick of forgetting to add an extra byte to the length of a string for the terminating \0. I used the Log function which is currently commented out.

The program builds a panel of controls with event handlers. Click the Change Label button to change the Label contents, tick the FPS box to show the frames per second in the caption. The second number is the number of GUI objects on screen. If you click the Dice logo, you’ll see 100 SDLImages added to the screen and the number of GUI objects increase to 108.

Remember every 1/60th of a second, the GUI is being fully redrawn. Keep clicking the Dice logo to add more randomly placed Dice Logos. On my PC, the frame rate remains at 60 fps until it’s showing 608 GUI objects when it drops to 51 fps. That’s redrawing 31,008 GUI controls each and every second. Keep clicking and at 3008 objects that’s still drawing at 12 fps, so 36,096 controls per second.

Full sources can be found on Github at sdlgui on Github.

Conclusion

This is just the first step. It needs more controls added, a better font or selection and some kind of skinning to make it look nice, perhaps with textured surfaces, rounded corners etc. But over 30,000 controls drawn per second is impressive!

Post a Comment

Your email address will not be published.