In this lesson, we build upon our work on creating a UI architecture. Our goal is to introduce a Button
class that has two properties:
Here’s our starting point. We have a Window
header file, which is close to what we built on our lesson on creating an SDL2Â window.
Our Window
class looks like this:
#pragma once
#include <SDL.h>
class Window {
public:
Window() {
SDL_Init(SDL_INIT_VIDEO);
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
600, 200, 0
);
SDLWindowSurface = SDL_GetWindowSurface(SDLWindow);
Update();
}
void Update() {
SDL_FillRect(
SDLWindowSurface,
nullptr,
SDL_MapRGB(SDLWindowSurface->format, 40, 40, 40)
);
}
void RenderFrame() {
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() {
return SDLWindowSurface;
}
private:
SDL_Window* SDLWindow;
SDL_Surface* SDLWindowSurface;
};
We’ve also included code from our previous lesson on creating a UI architecture.
Based on that lesson, we have classes for Layer
and EventReceiver
, and our main.cpp
pulls everything together:
#pragma once
#include <vector>
#include <SDL.h>
#include "EventReceiver.h"
class Layer {
public:
bool HandleEvent(const SDL_Event* Event) {
for (const auto Handler : Subscribers) {
if (Handler->HandleEvent(Event)) {
return true;
}
}
return false;
}
void SubscribeToEvents(EventReceiver* Receiver) {
Subscribers.push_back(Receiver);
}
private:
std::vector<EventReceiver*> Subscribers;
};
#pragma once
#include <SDL.h>
class EventReceiver {
public:
virtual bool HandleEvent(const SDL_Event* Event) {
return false;
}
};
#include <SDL.h>
#include "Window.h"
#include "Layer.h"
#include "EventReceiver.h"
int main() {
Window GameWindow;
Layer UI;
EventReceiver ExampleButton;
UI.SubscribeToEvents(&ExampleButton);
SDL_Event Event;
while(true) {
while(SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
if (UI.HandleEvent(&Event)) {
continue;
}
}
GameWindow.RenderFrame();
}
}
Compiling and running our code should generate a blank window:
To be compatible with our other systems, our Button
class should inherit from EventReceiver
.
To make our button eventually do something, we’ll also need to override the virtual HandleEvent
 function:
#pragma once
#include <SDL.h>
#include "EventReceiver.h"
class Button : public EventReceiver {
public:
bool HandleEvent(const SDL_Event* Event) override {
// TODO
return false;
}
};
Over in main
, lets update ExampleButton
to use this class:
#include <SDL.h>
#include "Window.h"
#include "Layer.h"
#include "EventReceiver.h"
#include "Button.h"
int main() {
Window GameWindow;
Layer UI;
EventReceiver ExampleButton;
Button ExampleButton;
UI.SubscribeToEvents(&ExampleButton);
// ... remaining code unchanged
}
Our application should still compile and run at this point.
In our lesson on Windows, we saw we could render a color to part of the screen, using SDL_FillRect
:
SDL_FillRect(
PointerToWindowSurface,
PointerToRectangle,
SDL_MapRGB(WindowSurfaceFormat, Red, Green, Blue)
);
Lets use this to make our button visible.
We want our button colour to be different, depending on whether or not the user is hovering their cursor over it.
Let's add an Update
function to our Button
class for this. We can call this function any time our button color needs to change:
private:
void Update() {
auto [r, g, b, a] { isHovered ? HoverColor : BGColor };
SDL_FillRect(
SDLWindowSurface,
&Rect,
SDL_MapRGB(SDLWindowSurface->format, r, g, b)
);
}
We need to provide the isHovered
, BackgroundColor
, HoverColor
, SDLWindowSurface
and Rect
arguments. Let's add private members to our class for them:
private:
// TODO: Update me based on mouse movement
bool isHovered { false };
// TODO: Initialise me on construction
SDL_Surface* SDLWindowSurface;
SDL_Color BGColor { 255, 50, 50, 255 };
SDL_Color HoverColor { 50, 50, 255, 255 };
SDL_Rect Rect { 50, 50, 50, 50 };
Here, we've made use of SDL_Rect
and SDL_Color
. These are simple types that SDL provide for storing rectangles and colors.
SDL_Rect
The arguments we’re passing to the SDL_Rect
constructor will cause our Rect
to have a horizontal position of 50
, a vertical position of 50
, a width of 50
, and a height of 50
.
The official documentation for SDL_Rect
is available here: https://wiki.libsdl.org/SDL_Rect
SDL_Color
The arguments we’re passing to the SDL_Color
constructor represent the quantity of our color's red, green, blue and alpha (opaqueness)Â channels.
Each channel has a range of 0-255. Therefore, the argument list 255
, 50
, 50
and 255
represents an opaque, red colour.
The official documentation for SDL_Color is available here: https://wiki.libsdl.org/SDL_Color
SDL_Surface*
Getting access to the SDL Window surface from our button will require a little more thought.
Out button needs a pointer to the SDL window surface. Arbitrary objects requiring access to important systems are pretty common, but we also need to be careful here.
Liberally allowing any object to talk to any other object is a recipe for disaster as our projects scale out.
It’s a good idea to standardize how our systems access each other and affect changes. A common approach is to create a single object, called something like Application
.
The application grants access to subsystems and state changes, in a controlled way.
Let's create our Application
class in a new header file, called Application.h
:
#pragma once
#include <SDL.h>
#include "Window.h"
class Application {
public:
Application(Window* Window) : mWindow { Window }
{}
SDL_Surface* GetWindowSurface() {
return mWindow->GetSurface();
}
void Quit() {
SDL_Event QuitEvent { SDL_QUIT };
SDL_PushEvent(&QuitEvent);
}
private:
Window* mWindow;
};
Let's update our Button class to accept a pointer to this application in its constructor. From there, we will set our SDLWindowSurface
private variable. We’ll also add a private variable to hold a pointer to the application, which we’ll need later.
Finally, we’ll also call Update
in the constructor, so our button triggers its first render:
#pragma once
#include <SDL.h>
#include "EventReceiver.h"
#include "Application.h"
class Button : public EventReceiver {
public:
Button(Application* App) :
SDLWindowSurface{App->GetWindowSurface()},
App{App}
{
Update();
}
// ... remaining code unchanged
private:
Application* App;
// ... remaining code unchanged
};
Back in main.cpp
, we need to connect everything up by:
Application
headerApplication
object, passing in a pointer to our Window
Application
into the constructor for our Button
#include <SDL.h>
#include "Window.h"
#include "Layer.h"
#include "Button.h"
#include "Application.h"
int main() {
Window GameWindow;
Application App { &GameWindow };
Layer UI;
Button ExampleButton { &App };
UI.SubscribeToEvents(&ExampleButton);
// ... remaining code unchanged
}
We should now see our button being rendered:
We’re almost done! The last task is to update the HandleEvent
function on our Button
 class.
The lesson on keyboard and mouse events laid the foundation here:
There are two events we care about. First, we need to listen for the click event, so our button can react to the user clicking on it. Secondly, we care about the mouse motion event so our button can react to the user hovering over it.
The click event is somewhat straightforward. The user clicked our button if three things are true:
SDL_MOUSEBUTTONDOWN
eventSDL_BUTTON_LEFT
We can check those three things, and trigger our function call to quit the application if they're all true. We also return true
from our HandleEvent
function, to indicate our button handled the event:
bool HandleEvent(const SDL_Event* Event) override {
if (
Event->type == SDL_MOUSEBUTTONDOWN &&
Event->button.button == SDL_BUTTON_LEFT &&
isHovered
) {
App->Quit();
return true;
} else if (
Event->type == SDL_MOUSEMOTION
) [[likely]] {
// Handle hovering
}
// If we get this far, this button didn't handle the event
return false;
}
Mouse motion is a little more difficult. We need to find if the user’s cursor ended up in the boundary of our button.
Let's create a separate function for that, and implement our logic there. We'll create a function that takes the cursor's x
and y
position, and returns true
if that position is within the bounds of our button's rectangle:
private:
bool IsWithinBounds(int x, int y) {
// 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;
// Inside rectangle
return true;
}
Back in HandleEvent
, we can now use this function to determine if the user’s cursor is currently hovering over our button after a mouse motion event.
As long as the user is currently hovering our button, their mouse motion events can be handled by our button, so we should return true
.
Additionally, we need to consider whether the user’s cursor has entered or left our button as a result of this event.
That is, we need to check if the boolean returned from IsWithinBounds
is different from our isHovered
member variable.
If they are different, that means we need to update our isHovered
variable to reflect the change, and update our button color.
Our implementation could look something like this:
// ... remaining HandleEvent code unchanged
} else if (
Event->type == SDL_MOUSEMOTION
) [[likely]] {
// Has the user enterred or left our button this frame?
if (isHovered != IsWithinBounds(
Event->motion.x, Event->motion.y
)) {
// If so, update our internal variable...
isHovered = !isHovered;
// ...and update our color
Update();
}
// Return true if the user is currently hovering our button
return isHovered;
}
// ... remaining HandleEvent code unchanged
With that, our button should be working! Hovering over it should change its color from red to blue, and clicking on it should quit our application.
Our complete code is available here:
#include <SDL.h>
#include "Window.h"
#include "Layer.h"
#include "Button.h"
#include "Application.h"
int main() {
Window GameWindow;
Application App { &GameWindow };
Layer UI;
Button ExampleButton { &App };
UI.SubscribeToEvents(&ExampleButton);
SDL_Event Event;
while(true) {
while(SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
if (UI.HandleEvent(&Event)) {
continue;
}
}
GameWindow.RenderFrame();
}
}
#pragma once
#include <SDL.h>
#include "Window.h"
class Application {
public:
Application(Window* Window) : mWindow { Window }
{}
SDL_Surface* GetWindowSurface() {
return mWindow->GetSurface();
}
void Quit() {
SDL_Event QuitEvent { SDL_QUIT };
SDL_PushEvent(&QuitEvent);
}
private:
Window* mWindow;
};
#pragma once
#include <SDL.h>
#include "EventReceiver.h"
#include "Application.h"
#include <iostream>
class Button : public EventReceiver {
public:
Button(Application* App) :
SDLWindowSurface{App->GetWindowSurface()},
App{App}
{
Update();
}
bool HandleEvent(const SDL_Event* Event) override {
if (
Event->type == SDL_MOUSEBUTTONDOWN &&
Event->button.button == SDL_BUTTON_LEFT &&
isHovered
) {
App->Quit();
} else if (
Event->type == SDL_MOUSEMOTION) [[likely]]
{
if (isHovered != IsWithinBounds(
Event->motion.x, Event->motion.y
)) {
isHovered = !isHovered;
Update();
}
return isHovered;
}
return false;
}
private:
bool IsWithinBounds(int x, int y) {
// 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;
// Inside rectangle
return true;
}
void Update() {
auto [r, g, b, a] { isHovered ? HoverColor : BGColor };
SDL_FillRect(
SDLWindowSurface,
&Rect,
SDL_MapRGB(SDLWindowSurface->format, r, g, b)
);
}
bool isHovered { false };
SDL_Color BGColor { 255, 50, 50, 255 };
SDL_Color HoverColor { 50, 50, 255, 255 };
SDL_Rect Rect { 50, 50, 50, 50 };
Application* App { nullptr };
SDL_Surface* SDLWindowSurface { nullptr };
};
#pragma once
#include <SDL.h>
class EventReceiver {
public:
virtual bool HandleEvent(const SDL_Event* Event) {
return false;
}
};
#pragma once
#include <vector>
#include <SDL.h>
#include "EventReceiver.h"
class Layer {
public:
bool HandleEvent(const SDL_Event* Event) {
for (const auto Handler : Subscribers) {
if (Handler->HandleEvent(Event)) {
return true;
}
}
return false;
}
void SubscribeToEvents(EventReceiver* Receiver) {
Subscribers.push_back(Receiver);
}
private:
std::vector<EventReceiver*> Subscribers;
};
#pragma once
#include <SDL.h>
class Window {
public:
Window() {
SDL_Init(SDL_INIT_VIDEO);
SDLWindow = SDL_CreateWindow(
"Hello Window",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
600, 150, 0
);
SDLWindowSurface = SDL_GetWindowSurface(SDLWindow);
Update();
}
void Update() {
SDL_FillRect(
SDLWindowSurface,
nullptr,
SDL_MapRGB(SDLWindowSurface->format, 40, 40, 40)
);
}
void RenderFrame() {
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() {
return SDLWindowSurface;
}
private:
SDL_Window* SDLWindow { nullptr };
SDL_Surface* SDLWindowSurface { nullptr };
};
Up next, we’ll add the ability to render images within our application!
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games