Step-by-Step SDL Basic UI Controls: UIRadioButton

sdl-ui-control-radiobutton

In previous articles, we’ve implemented common UI controls like UIButton and UICheckBox. This time, we’ll continue expanding our UI control system with a very common component used in many applications: the RadioButton.

Radio buttons are typically found in forms and settings interfaces, where they’re used to select a single option from a group. The key points for implementing one are:

  • Display State: It has two states: checked and unchecked.
  • Grouping Mechanism: Within a single group, only one radio button can be selected at a time.
  • Event Handling: When the state changes, it should fire an event to notify the application’s logic.

In this article, we’ll build the UIRadioButton control step-by-step and show how to manage groups with UIRadioButtonGroup.


1. Basic Class Definition

First, we define the UIRadioButton class, which inherits from UIControl to get basic rendering and event handling capabilities.

C++

#pragma once
#include "UIControl.h"

// We'll define two simple global textures for the checked and unchecked states
extern SDL_Texture* rbtn_checked;
extern SDL_Texture* rbtn_unchecked;

class UIRadioButtonGroup;
class UIRadioButton : public UIControl
{
public:
    UIRadioButton();
    UIRadioButton(const std::string& _text);
    ~UIRadioButton();

    void Render() override;
    bool Handle(SDL_Event* e) override;
    void OnTextChanged(const std::string& old) override;

    bool IsChecked() const { return m_ischecked; }
    void SetCheck(bool _check);

    std::string GroupName() const;
    void SetGroupName(const std::string& _gp);
    void SetGroup(UIRadioButtonGroup* _gp);

private:
    bool m_ischecked{ false };              // Is it currently checked?
    UIRadioButtonGroup* m_gropu{ nullptr }; // The group it belongs to
    std::string m_gpName;                   // The group name
    int m_radioindex{ 0 };                  // Control index
};

Here are a few key points:

  • m_ischecked is a flag to indicate if the button is selected.
  • m_gropu is a pointer to the group it belongs to, which is used to manage mutual exclusivity with other buttons in the group.
  • GroupName() allows us to manage groups using a string, making it easy to dynamically modify them within the UI system.

2. Texture Loading and Initialization

We need two images: one for the checked state and one for the unchecked state.

C++

#define RBTN_CHECK_RES "D:/Projects/res/checked.jpeg"
#define RBTN_UNCHECK_RES "D:/Projects/res/uncheck.jpeg"

SDL_Texture* rbtn_checked{ nullptr };
SDL_Texture* rbtn_unchecked{ nullptr };

UIRadioButton::UIRadioButton(const std::string& _text)
    : UIControl()
{
    m_text = _text;
    static unsigned int _ix = 1;
    _ix++;
    m_radioindex = _ix;

    if (!rbtn_checked)
        rbtn_checked = sRender->LoadTextureFromFile(RBTN_CHECK_RES);

    if (!rbtn_unchecked)
        rbtn_unchecked = sRender->LoadTextureFromFile(RBTN_UNCHECK_RES);
}

This code ensures that the textures are loaded only once, preventing memory waste.


3. Rendering Logic

Rendering involves two main parts:

  1. Drawing the text (the button’s label).
  2. Drawing the correct image based on the button’s state.

C++

void UIRadioButton::Render()
{
    auto rc = GetGlobalRect();
    auto rcCheck = SDL_Rect{ rc.x, rc.y, CHECK_BOX_SIZE, CHECK_BOX_SIZE };
    auto [w, h] = sRender->GetTextSize(m_text.c_str(), m_fontSize);
    int dism = (rc.h - h) / 2;
    auto rcText = SDL_Rect{ rcCheck.x + rcCheck.w + 4, rc.y + dism, w, h };
    sRender->DrawString(m_text.c_str(), m_fontSize, rcText, m_textColor);

    if (m_ischecked)
    {
        sRender->DrawImage(rbtn_checked, rcCheck.x, rcCheck.y);
    }
    else
    {
        sRender->DrawImage(rbtn_unchecked, rcCheck.x, rcCheck.y);
    }
}

4. Event Handling

The click logic for a radio button is very simple:

  • If the button is currently unchecked, we notify its group to make it checked and uncheck all other buttons in the group.

C++

bool UIRadioButton::Handle(SDL_Event* e)
{
    if (e->type == SDL_MOUSEBUTTONDOWN)
    {
        if (e->button.button == SDL_BUTTON_LEFT)
        {
            if (!m_ischecked)
            {
                m_gropu->SetRadioButtonCheck(this, !m_ischecked);
                FireEvent(EventType::ET_CHECKCHANGED, nullptr);
            }
        }
    }
    return UIControl::Handle(e);
}

5. Implementing the Group: UIRadioButtonGroup

To ensure the mutual exclusivity effect, we need a UIRadioButtonGroup.

C++

class UIRadioButtonGroup
{
public:
    void SetRadioButtonCheck(UIRadioButton* btn, bool _check);
    void Add(UIRadioButton* btn);
    void Remove(UIRadioButton* btn);
    int Count() const;

private:
    std::vector<UIRadioButton*> m_rbtns;
};

void UIRadioButtonGroup::SetRadioButtonCheck(UIRadioButton* btn, bool _check)
{
    for (auto& c : m_rbtns)
    {
        if (c == btn)
        {
            c->SetCheck(_check);
        }
        else
        {
            c->SetCheck(!_check);
        }
    }
}

With this, only one radio button in a group will ever be in the checked state.


6. Usage Example

Let’s see how to use our new controls:

C++

auto rbtn1 = std::shared_ptr<UIRadioButton>(new UIRadioButton());
rbtn1->SetPosition({ 0,0 });
rbtn1->SetText("user");
rbtn1->SetCheck(true);
rbtn1->SetGroupName("group1");
rbtn1->AddEvent(EventType::ET_CHECKCHANGED, [=](void*) 
{
    if (rbtn1->IsChecked())
    {
        char szmsg[64];
        sprintf_s(szmsg, "user is checked");
        m_lbmsg->SetText(szmsg);
    }
});
AddNode(rbtn1);

auto rbtn2 = std::shared_ptr<UIRadioButton>(new UIRadioButton());
rbtn2->SetPosition({ 0,25 });
rbtn2->SetText("administrator");
rbtn2->SetCheck(false);
rbtn2->AddEvent(EventType::ET_CHECKCHANGED, [=](void*)
{
    if (rbtn2->IsChecked())
    {
        char szmsg[64];
        sprintf_s(szmsg, "administrator is checked");
        m_lbmsg->SetText(szmsg);
    }
});
rbtn2->SetGroupName("group1");
AddNode(rbtn2);

After running this code, you’ll get a familiar radio button group.


7. Summary and Next Steps

With this, we have completed the implementation of UIRadioButton. The key takeaways are:

  • Group Exclusivity (achieved through UIRadioButtonGroup).
  • State Toggling (checked / unchecked).
  • Event-Driven Behavior (triggering callbacks on state changes).

You can find the project code for this chapter here: https://github.com/84378996/simple_sdl_gui_tutorials/tree/main/tutorials07

See the final demo effect here:

Leave a Reply