Building Interactive Buttons

Explore techniques for building UI components that respond to user input
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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.

Reviewing the UI Components

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){
    // ...
  }
};

Creating a Button Class

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:

Screenshot of our program

Currently, it’s not doing anything beyond what a basic Rectangle would, so lets add some capabilities to it.

Handling Hover Events

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:

  • Determining if a set of x and y coordinates are within the bounds of the rectangle
  • Customising the color of the rectangle

Let’s add these capabilities to our Rectangle:. We’ll make the following changes:

  • Add a Color member to store what color the rectangle should render as, and a SetColor() setter to let people change it
  • Add an optional constructor parameter to let consumers specify the starting color
  • Update Render() to make use of the Color
  • Add an 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:

  • Provide a color to the Rectangle constructor. We’ll use green (0, 255, 0)
  • Update HandleEvent() to forward any SDL_MOUSEMOTION events to the new HandleMouseMotion() function
  • Within HandleMouseMotion(), 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.

Surface vs Window Coordinates

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.

Handling Click Events

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:

  • Add new HandleLeftClick() and HandleRightClick() functions. The functions don’t do anything, but derived classes can override them to provide implementations.
  • Updating HandleEvent() to call a new HandleMouseButton() function
  • Within HandleMouseButton(), 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();
      }
    }
  }
};

Creating Derived Buttons

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};
};

Expanding Capability

Let's explore two more examples of how we can enhance our UI components to make them more powerful and flexible

Disabling and Enabling Buttons

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};
};

Mouse Hover Reactions

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.

Summary

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.

Was this lesson useful?

Next Lesson

Custom User Events

Discover how to use the SDL2 event system to handle custom, game-specific interactions
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, Unlimited Access
Implementing User Interaction
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Free, unlimited access

This course includes:

  • 67 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Custom User Events

Discover how to use the SDL2 event system to handle custom, game-specific interactions
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved