31 Dec

Creating a Simple GUI from Scratch

Introduction

More often than not, the biggest obstacle for a programmer is adding a GUI to a otherwisely ready application. This probably is because you generally can create a program that inputs and outputs text with a few lines of code in any language. However, if you don’t count MessageBox() and other similar helper APIs that are available in most windowing systems and that are as simple to use as gets() and printf(), it’s a unnecessarily big step to change a command line program into a program that outputs the exact same text but with complex command line arguments replaced by a few buttons and a window with the outputted text.

However, if you are developing a program that already draws something on the screen, it’s really easy to add simple mouse interactivity. This is especially true if you are using SDL (even if it’s extremely bare bones in these things!) and already use SDL_rect (your standard rectangle) to draw things. You can simply change the draw routine so that it takes one more argument which would be the SDL_event struct you’re already using to check for a quit message and so on. Then for every object you are drawing on screen, check if the event if a mouse button down event and that the coordinates are inside the rectangle that will be drawn. This eliminates completely the need for having to plan a separate system that interprets the mouse events. It’s sort of piggybacking on the existing code with minimal changes to the existing code.

Now, you might already think that is way too simplistic and lazy but think again. Not many programs need a more complex GUI with multiple movable windows and so on. If you really do need that, then be my guest and create a exhaustive windowing system or take the time to learn an existing system. But it is still overhead and you have to do comparatively a lot of work before any real results. At least I hate that kind of mental overhead. And while this whole idea of combining the drawing and the event processing sounds like a bit of a hack, it really isn’t a “hack” as in patching something you will probably have to replace with a better solution later. It’s just a different way of doing almost the same thing. With less overhead.

I have used this approach in my latest project and from direct experience I know it is possible to create scrollbars, scrolling text fields, text input fields, message boxes and pretty much anything I have needed. And it’s not too much code either even if you have to create absolutely everything from nothing. In other words I do not feel limited or burdened.

How to do it

As explained above, you most likely are drawing to a specific region on the screen for every object you need to check for mouse clicks. It is not necessary to have any kind of array where those regions are. You need to make sure that for every mouse click event runs the draw loop once and that the mouse click event gets passed to the draw routine. Then check if the coordinates are inside the draw region.

This is where the part starts that it admittedly gets a bit hacky: if a button is pressed, the event that the button triggers will be run in the middle of the draw loop (unless you somehow buffer the events). In most cases this doesn’t matter at all but it could be that what is on the screen is not exactly how it really is; you might have two selected items for a duration of one frame and so on. For the most part this doesn’t matter since you are getting results with very little overhead.

Consider this example.

void draw_stuff()
{
  SDL_Rect position = {10, 10, 40, 40};

  SDL_BlitRect(button_gfx, NULL, screen, &position);
}

void draw_stuff_and_check_events(SDL_Event *event)
{
  SDL_Rect position = {10, 10, 40, 40};

  if (event->type == SDL_MOUSEBUTTONDOWN 
   && event->button.x >= position.x
   && event->button.y >= position.y  
   && event->button.x < position.x + position.w 
   && event->button.y < position.y + position.h)
     button_event();

  SDL_BlitRect(button_gfx, NULL, screen, &position);
}

void event_loop()
{
  SDL_Event event;

  while (SDL_PollEvent(&event))
  {
    if (event.type == SDL_QUIT) break;

    draw_stuff(&event);
  }
}

From the above example, the benefits of this approach are obvious. The event checking can simply be injected anywhere in the code as long as you have the event and a region.

Solutions to common problems

One downside in this is that if you draw multiple overlapping regions, the region drawn last will be the one that is visible on the screen but the click is handled by the first region. Our event checking sees the regions from the other side of the screen. In such cases you can first iterate the regions in reverse order checking the event and then draw them in the correct order. An example in the mentioned project is the menu, submenus often overlap the parent menu; I solved that by first going through the menus in the order the user sees them and then drawing them back to front.

Dragging items is easy: simply check for mouse motion events and if the button is held down and the mouse is moved, just adjust the position of the matching region. You do not need any special “drag starts now” and “drag ends now, update objects” phases. However, this simplistic method is subject to the previous issue if you move a region over one that seems to be under it. You can also simply record the clicked object when the mouse button is pressed and make the motion events match only the selected region.

FAQ

I absolutely need a separate window

This is possible as long as the window can be modal (like a message or open file dialog that takes over from the window behind it). You simply jump to another event loop until the new window is closed, much like it’s done in the Windows API with GetOpenFileName() or the MessageBox() mentioned earlier. Then it’s just a matter of drawing the new window and checking for the events normally.

Is this reusable?

Of course. You could create a small library that has basic functionality and helper functions. A practical example can be seen here.

Even if you can use the event checking straight in the source code, you can still define the regions in an array and link to relevant event handlers.

Conclusion

Combining the drawing and the event processing code can save time in the short term. Many common GUI elements are perfectly possible to replicate. The idea described above should be considered if a project needs mouse interaction and external libraries are not available, the conversion of existing code seems expensive or the learning curve is too steep compared to the future benefits. Nonetheless, in borderline cases, it can be well worth prototyping due to the minimal overhead and developing considerations.