Welcome to this lesson on building interactive UI components using SDL in C++. In this tutorial, we'll explore how to create dynamic buttons that respond to user input.
We'll cover topics such as event handling, creating custom button classes, and implementing hover and click behaviors.
The topics in this lesson build on our earlier work, in particular the previous lesson where we set up our UI architecture.
Our main.cpp
looks like the following, where we implement a standard event loop. For the purposes of this lesson, the key thing to note is that we have a UI
object called GameUI
.
This object is receiving all events through the HandleEvent()
method, and is being asked to Render()
onto the window surface every frame:
#include <SDL.h>
#include "UI.h"
class Window {/*...*/};
int main(int argc, char** argv){
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
UI GameUI;
SDL_Event Event;
bool shouldQuit{false};
while (!shouldQuit) {
while (SDL_PollEvent(&Event)) {
GameUI.HandleEvent(Event);
if (Event.type == SDL_QUIT) {
shouldQuit = true;
}
}
GameWindow.Render();
GameUI.Render(GameWindow.GetSurface());
GameWindow.Update();
}
SDL_Quit();
return 0;
}
Both of these methods are currently empty in the UI
class, but we’ll add to them soon:
// UI.h
#pragma once
class UI {
public:
void HandleEvent(const SDL_Event& E){
// ...
};
void Render(SDL_Surface* Surface){
// ...
}
};
To create our buttons, we’ll rely on a Rectangle
class which we also introduced in the previous lesson.
Rectangle
objects currently receive a position (x
and y
) and size (w
and h
) in their constructor. They construct an SDL_Rect
from these arguments, and every call to Render()
fills the corresponding area of a SDL_Surface
with a solid color:
// Rectangle.h
#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(int x, int y, int w, int h)
: Rect{x, y, w, h}{}
void Render(SDL_Surface* Surface){
SDL_FillRect(
Surface, &Rect, SDL_MapRGB(
Surface->format, 255, 0, 0
)
);
}
private:
SDL_Rect Rect{0, 0, 0, 0};
};
Let’s create our Button
class by inheriting from Rectangle
. It receives the same 4 arguments and forwards them to the Rectangle
constructor. We also have a HandleEvent()
method, which is currently not doing anything:
// Button.h
#pragma once
#include <SDL.h>
#include "Rectangle.h"
class Button : public Rectangle {
public:
Button(int x, int y, int w, int h)
: Rectangle{x, y, w, h}{}
void HandleEvent(const SDL_Event& E){
// ...
};
};
Finally, let’s create a Button
and add it to our UI
. Our UI
will forward events to our Button
by calling HandleEvent()
, and will forward render requests by calling Render()
(which Button
is inheriting from Rectangle
):
// UI.h
#pragma once
#include "Button.h"
class UI {
public:
void Render(SDL_Surface* Surface){
MyButton.Render(Surface);
}
void HandleEvent(const SDL_Event& E) {
MyButton.HandleEvent(E);
}
// x = 50, y = 50, w = 50, h = 50
Button MyButton{50, 50, 50, 50};
};
Running our game, we should now see the button rendered on our screen:
Currently, it’s not doing anything beyond what a basic Rectangle
would, so lets add some capabilities to it.
We’ll start by giving buttons the capability to change color when the player hovers their mouse over them.
When planning how we will add a new capability, it’s helpful to break down the requirements into parts. This is particularly true if we can identify some parts that are more suitable to be added to the base class.
When we add capabilities to the base class, those capabilities becomes available to all subtypes of Rectangle
, not just Button
. This makes our UI components more powerful over time.
To implement hover reactions, we can identify two aspects that are generally relevant to rectangles, so could be added to our Rectangle
class:
Let’s add these capabilities to our Rectangle
:. We’ll make the following changes:
Color
member to store what color the rectangle should render as, and a SetColor()
setter to let people change itRender()
to make use of the Color
IsWithinBounds()
function to determine if a position overlaps with the rectangle.#pragma once
#include <SDL.h>
class Rectangle {
public:
Rectangle(
int x, int y, int w, int h,
SDL_Color Color = {0, 0, 0, 255})
: Rect{x, y, w, h}, Color{Color}{}
virtual void Render(SDL_Surface* Surface){
SDL_FillRect(
Surface, &Rect, SDL_MapRGB(
Surface->format,
Color.r, Color.g, Color.b
)
);
}
void SetColor(SDL_Color C){ Color = C; }
bool IsWithinBounds(int x, int y) const{
// Too far left
if (x < Rect.x) return false;
// Too far right
if (x > Rect.x + Rect.w) return false;
// Too high
if (y < Rect.y) return false;
// Too low
if (y > Rect.y + Rect.h) return false;
// Within bounds
return true;
}
private:
SDL_Rect Rect{0, 0, 0, 0};
SDL_Color Color{0, 0, 0, 0};
};
Implementing the state change when the user’s cursor is hovering the component is a behavior that’s more specific to buttons. So, we’ll add that logic to the Button
class. Let’s make the following changes:
Rectangle
constructor. We’ll use green (0, 255, 0)HandleEvent()
to forward any SDL_MOUSEMOTION
events to the new HandleMouseMotion()
functionHandleMouseMotion()
, set the rectangle’s color to red (255, 0, 0) if the cursor is within the bounds of the rectangle, and back to green (0, 255, 0) otherwise#pragma once
#include "Rectangle.h"
class Button : public Rectangle {
public:
Button(int x, int y, int w, int h)
: Rectangle{x, y, w, h, {0, 255, 0}}
{}
void HandleEvent(const SDL_Event& E){
if (E.type == SDL_MOUSEMOTION) {
HandleMouseMotion(E.motion);
}
}
protected:
void HandleMouseMotion(
const SDL_MouseMotionEvent& E){
if (IsWithinBounds(E.x, E.y)) {
SetColor({255, 0, 0});
} else {
SetColor({0, 255, 0});
}
}
};
Running our program, we should now see that our button is rendered in green, and changes to red when our cursor is over it.
In this section, the position and size stored in the SDL_Rect
within our Rectangle
class is relative to the SDL_Surface
it is rendered on, whilst the mouse coordinates reported in the SDL_MouseMotionEvent
are relative to the SDL_Window
the user is hovering over.
Our implementation assumes these values will be equivalent. In this case, that assumption is valid, as we only have one window, and the SDL_Surface
passed to Render()
is the surface of that window.
However, this is not always the case, and more complex programs may need more elaborate solutions to compare positions across different coordinate systems.
Let’s allow our buttons to react to being clicked. However, in the base Button
class, we don’t know what behavior we need to perform in response to a click.
Instead, our goal can be to let derived classes implement their behavior as easily as possible.
To accomplish this, we can take care of the complexity of understanding when the player clicked the button, call simple HandleLeftClick()
and HandleRightClick()
functions at the appropriate time.
We’ll mark these functions as virtual
, allowing the base classes to override them. Our changes are:
HandleLeftClick()
and HandleRightClick()
functions. The functions don’t do anything, but derived classes can override them to provide implementations.HandleEvent()
to call a new HandleMouseButton()
functionHandleMouseButton()
, analyse the SDL_MouseButtonEvent
to determine it represents the user clicking on our button. If they did, call HandleLeftClick()
or HandleRightClick()
as appropriate.#pragma once
#include "Rectangle.h"
class Button : public Rectangle {
public:
Button(int x, int y, int w, int h) : Rectangle
{x, y, w, h, {0, 255, 0}}{}
void HandleEvent(const SDL_Event& E){
if (E.type == SDL_MOUSEMOTION) {
HandleMouseMotion(E.motion);
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
HandleMouseButton(E.button);
}
}
protected:
virtual void HandleLeftClick(){}
virtual void HandleRightClick(){}
private:
void HandleMouseMotion(
const SDL_MouseMotionEvent& E){
if (IsWithinBounds(E.x, E.y)) {
SetColor({255, 0, 0});
} else {
SetColor({0, 255, 0});
}
}
void HandleMouseButton(
const SDL_MouseButtonEvent& E) {
if (IsWithinBounds(E.x, E.y)) {
const Uint8 Button{E.button};
if (Button == SDL_BUTTON_LEFT) {
HandleLeftClick();
} else if (Button == SDL_BUTTON_RIGHT) {
HandleRightClick();
}
}
}
};
With our base Button
class now set up, we can create new classes that derive from it, and are designed to implement the specific behaviors required of our application.
We simply need to override the HandleLeftClick()
and / or the HandleRightClick()
methods as needed:
#pragma once
#include "Button.h"
class DerivedButton : public Button {
public:
DerivedButton(int x, int y, int w, int h)
: Button{x, y, w, h} {}
protected:
void HandleLeftClick() override{
std::cout << "I have been left-clicked\n";
}
void HandleRightClick() override {
std::cout << "I have been right-clicked\n";
}
};
Often, our buttons use the SDL event queue to report when the player clicks them, allowing other parts of our application to react to the action.
In the next lesson, we’ll see how we can define custom event types that correspond to specific to the actions that players can perform within our game.
For now, we can see this principle in action by pushing an SDL_QUIT
event onto the queue:
#pragma once
#include <SDL.h>
#include "Button.h"
class QuitButton : public Button {
public:
QuitButton(int x, int y, int w, int h)
: Button{x, y, w, h} {}
protected:
void HandleLeftClick() override {
SDL_Event Quit{SDL_QUIT};
SDL_PushEvent(&Quit);
}
};
If the player left clicks this button, our main application loop will receive an SDL_QUIT
event, triggering the loop to end and our program to quit.
Remember, to add any of these components to our UI, we can construct them in our UI
class, and add them to the Render()
and HandleEvent()
methods:
#pragma once
#include "Button.h"
#include "QuitButton.h"
class UI {
public:
void Render(SDL_Surface* Surface){
MyButton.Render(Surface);
MyQuitButton.Render(Surface);
}
void HandleEvent(const SDL_Event& E) {
MyButton.HandleEvent(E);
MyQuitButton.HandleEvent(E);
}
Button MyButton{50, 50, 50, 50};
QuitButton MyQuitButton{150, 50, 50, 50};
};
Let's explore two more examples of how we can enhance our UI components to make them more powerful and flexible
A common requirement in user interfaces is the ability to enable or disable buttons. A disabled button remains visible but doesn't react to mouse events.
This feature is likely to be useful across various Button
subtypes, so we'll add it to the base class:
#pragma once
#include "Rectangle.h"
class Button : public Rectangle {
public:
// ...
void HandleEvent(const SDL_Event& E){
if (isDisabled) return;
if (E.type == SDL_MOUSEMOTION) {
HandleMouseMotion(E.motion);
} else if (E.type == SDL_MOUSEBUTTONDOWN) {
HandleMouseButton(E.button);
}
}
protected:
// ...
void SetIsDisabled(bool newValue){
isDisabled = newValue;
}
private:
// ...
bool isDisabled{false};
};
By adding this functionality to the base Button
class, we allow all derived classes to take advantage of this feature.
Here's an example of how we might use this in a LimitedClickButton
class, which becomes disabled after the user clicks it three times:
#pragma once
#include <SDL.h>
#include "Button.h"
class LimitedClickButton: public Button {
public:
LimitedClickButton(int x, int y, int w, int h)
: Button{x, y, w, h} {}
protected:
void HandleLeftClick() override {
--ClicksRemaining;
if (ClicksRemaining == 0){
SetIsDisabled(true);
}
}
private:
int ClicksRemaining{3};
};
While our base Button
class already handles mouse hover events, we can improve it to allow derived classes to easily implement custom hover behaviors.
Let's refactor our HandleMouseMotion()
function to call new HandleMouseEnter()
and HandleMouseExit()
virtual functions:
#pragma once
#include "Rectangle.h"
class Button : public Rectangle {
public:
// ...
protected:
// ...
virtual void HandleMouseEnter(){
SetColor({255, 0, 0});
}
virtual void HandleMouseExit(){
SetColor({255, 0, 0});
}
private:
void HandleMouseMotion(
const SDL_MouseMotionEvent& E){
if (IsWithinBounds(E.x, E.y)) {
SetColor({255, 0, 0});
HandleMouseEnter();
} else {
SetColor({0, 255, 0});
HandleMouseExit();
}
}
};
With this refactoring, button subtypes can now easily implement custom behaviors when the user hovers over them. For instance, this BlueHoverButton
uses blue as the hover color instead of the default red:
#pragma once
#include <SDL.h>
#include "Button.h"
class BlueHoverButton: public Button {
public:
BlueHoverButton(int x, int y, int w, int h)
: Button{x, y, w, h} {}
protected:
virtual void HandleMouseEnter(){
SetColor({0, 0, 255});
}
};
Remember, if you want to extend the inherited behaviors rather than replace them entirely, you can call the base implementation from your override:
#pragma once
#include <SDL.h>
#include "Button.h"
class CustomHoverButton: public Button {
public:
CustomHoverButton(int x, int y, int w, int h)
: Button{x, y, w, h} {}
protected:
virtual void HandleMouseEnter(){
// Do the standard stuff
Button::HandleMouseEnter();
// Do extra stuff, too
std::cout << "Extra Stuff";
}
};
These examples demonstrate how we can continue to expand and customize our UI components to meet the specific needs of our application while maintaining a clean and extensible design.
In this lesson, we've explored the process of creating interactive UI components using SDL and C++. We started by reviewing our existing UI architecture and then delved into creating a button class that inherits from our Rectangle
class.
We implemented hover and click event handling, allowing our buttons to change appearance and respond to user input.
We also learned how to create derived button classes with custom behaviors, such as a quit button and a limited-click button. Additionally, we expanded our button capabilities by adding features like enabling/disabling buttons and customizing hover reactions.
These techniques provide a solid foundation for building more complex and interactive user interfaces in your SDL-based applications.
In the next lesson, we'll explore how to register custom events, allowing us to use SDL's event loop for behaviors specific to our games. This will enable even more sophisticated interactions and game-specific functionality in our UI components.
Explore techniques for building UI components that respond to user input
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games