
After implementing controls like UIButton and UITextField, this article will focus on a very common UI element: the ListBox. It’s widely used in interface development for scenarios like file selection, menu options, and displaying data lists.
The core features of UIListBox
include:
- Dynamically adding and removing list items.
- Mouse click selection with item highlighting.
- Mouse wheel scrolling to view content that exceeds the display area.
- Event callbacks for seamless interaction with external logic.
Let’s dive into the implementation and usage of this control step by step.
I. Data Structure Definition
First, we need a ListItem
struct to hold the data for each row:
C++
struct ListItem
{
std::string m_text; // The text to display
SDL_Texture* m_icon; // Optional icon (not used in this example)
bool m_checked; // Checked state (for future expansion)
void* data; // User-defined data
};
The UIListBox
internally maintains a std::vector<ListItem>
as its actual data source.
II. Basic Class Framework
UIListBox
inherits from UIScrollView
, so it naturally has scrollbar support. The key members of the class are as follows:
C++
class UIListBox : public UIScrollView
{
public:
UIListBox();
~UIListBox();
void AddItem(const ListItem& item);
void AddItem(const std::string& item);
void RemoveAt(size_t _index);
void RemoveAll();
int GetItemCount() const;
inline void SetSelectedIndex(int index) { m_selectedIndex = index; }
inline int GetSelectedIndex() const { return m_selectedIndex; }
const ListItem* GetSelectedItem() const;
void SetItemData(size_t ix, void* data);
void* GetItemData(size_t ix);
void SetItemText(size_t ix, const std::string& _name);
void SetItemSelected(int ix);
void Render() override;
bool Handle(SDL_Event* e) override;
void OnMove(int x, int y) override;
protected:
std::vector<ListItem> m_items; // List of items
int m_selectedIndex{ -1 }; // Index of the currently selected item
SDL_Point m_ptHit;
SDL_Point m_ptThum;
int m_offsetHit{ 0 };
};
Key Points:
m_items
stores all the list entries.m_selectedIndex
tracks the index of the currently selected item.- The scrolling functionality is achieved by combining
SetLineCount
andm_scrollOffset
with theUIScrollView
base class.
III. Adding and Removing Items
There are two ways to add items: by passing a ListItem
struct or just a string.
Adding Items:
C++
void UIListBox::AddItem(const ListItem& item)
{
m_items.push_back(item);
SetLineCount(m_items.size());
}
void UIListBox::AddItem(const std::string& item)
{
m_items.push_back({ item, nullptr, false, 0 });
SetLineCount(m_items.size());
}
Removing and Clearing Items:
C++
void UIListBox::RemoveAt(size_t _index)
{
m_items.erase(m_items.begin() + _index);
SetLineCount(m_items.size());
if (m_selectedIndex >= _index)
{
m_selectedIndex--;
}
}
void UIListBox::RemoveAll()
{
m_items.clear();
SetLineCount(m_items.size());
}
IV. Rendering Logic
The rendering process has three main steps:
- Drawing the background and border.
- Calculating the range of visible items based on the scroll position.
- Drawing the items, with the selected item highlighted.
C++
void UIListBox::Render()
{
auto rcClient = GetGlobalRect();
sRender->FillRect(rcClient, SDL_Color{ 255,255,255,255 });
sRender->DrawRect(rcClient, SDL_Color{ 50, 157, 77, 255 });
__super::Render();
SDL_Rect _rc = rcClient;
SDL_RenderSetClipRect(sRender->GetRender(), &_rc);
int startItem = m_scrollOffset / (m_lineheight+m_linespace);
int endItem = SDL_min(startItem + m_size.y / (m_lineheight + m_linespace), GetItemCount());
for (int i = startItem; i < endItem; ++i) {
SDL_Rect itemRect = { rcClient.x, rcClient.y + (i - startItem) * (m_lineheight + m_linespace), m_size.x - _BAR_SIZE, (m_lineheight + m_linespace) };
if (m_selectedIndex == static_cast<int>(i)) {
sRender->FillRect(itemRect, SDL_Color{ 200, 200, 200, 255 });
}
SDL_Color textColor = (m_selectedIndex == static_cast<int>(i)) ? SDL_Color{ 255, 255, 255, 255 } : SDL_Color{ 0, 0, 0, 255 };
auto [textW, textH] = sRender->GetTextSize(m_items[i].m_text.c_str(), m_fontSize);
sRender->DrawString(m_items[i].m_text.c_str(), m_fontSize, SDL_Point{ itemRect.x + 5, itemRect.y + (itemRect.h - textH) / 2 });
}
auto rcWnd = sRender->GetClientRect();
SDL_RenderSetClipRect(sRender->GetRender(), &rcWnd);
}
V. Event Handling
Event handling is divided into two types:
- Mouse Clicks: Determine the item corresponding to the click position and update
m_selectedIndex
. - Mouse Wheel: Scroll the list up or down.
C++
bool UIListBox::Handle(SDL_Event* e)
{
if (e->type == SDL_MOUSEBUTTONDOWN)
{
SDL_Point pt{ e->button.x,e->button.y };
auto rcClient = GetGlobalRect();
SDL_Rect rc{ rcClient.x,rcClient.y, m_size.x - _BAR_SIZE, m_size.y };
if (SDL_PointInRect(&pt,&rc))
{
int clickedItem = (int)(pt.y + m_scrollOffset - rcClient.y) / (m_lineheight + m_linespace);
if (clickedItem >= 0 && clickedItem < GetItemCount())
{
FireEvent(EventType::ET_ITEM_CLICK, &clickedItem);
if (m_selectedIndex != clickedItem)
{
m_selectedIndex = clickedItem;
FireEvent(EventType::ET_ITEMSELECTIONCHANGED, &m_selectedIndex);
}
}
return true;
}
}
else if (e->type == SDL_MOUSEWHEEL)
{
if (e->wheel.y > 0) OnScrollUp();
if (e->wheel.y < 0) OnScrollDown();
return true;
}
return __super::Handle(e);
}
Using FireEvent
, external logic can easily receive click and selection change events.
VI. Usage Example
Here’s a complete usage scenario:
C++
auto listbox = std::shared_ptr<UIListBox>(new UIListBox);
listbox->SetPosition({ 340, 20 });
listbox->SetSize({ 120,150 });
listbox->AddEvent(EventType::ET_ITEMSELECTIONCHANGED, [=](void*)
{
int ix = listbox->GetSelectedIndex();
char txt[64];
sprintf_s(txt, "listbox item at index %d selected", ix);
m_lbmsg->SetText(txt);
});
AddNode(listbox);
// Add button
auto btn = std::shared_ptr<UIButton>(new UIButton);
btn->SetText("AddItem");
btn->SetPosition({ 340,175 });
static int itemindex = 0;
btn->AddClick([=](void*)
{
char itemtext[64];
sprintf_s(itemtext, "item %d", ++itemindex);
listbox->AddItem(itemtext);
});
AddNode(btn);
// Delete button
btn = std::shared_ptr<UIButton>(new UIButton);
btn->SetText("RemoveSelectedItem");
btn->SetPosition({ 340,205 });
btn->SetSize({ 150,25 });
btn->AddClick([=](void*)
{
int sel = listbox->GetSelectedIndex();
if (sel >= 0)
listbox->RemoveAt(sel);
});
AddNode(btn);
Final Result:
- Clicking “AddItem” dynamically adds new items.
- Selecting an item triggers an event, updating the prompt text.
- Clicking “RemoveSelectedItem” deletes the currently selected item.
VII. Summary
With UIListBox
, we’ve created a fully functional list control that supports:
- Adding, removing, and modifying items.
- Mouse clicks and mouse wheel scrolling.
- Selection state and event notifications.
Together with the previously implemented UIButton
and UITextField
controls, we are gradually building a small SDL UI framework.
Future Enhancements:
The following features are not yet implemented but could be added:
- Displaying icons (
ListItem::m_icon
). - Check Box mode (
ListItem::m_checked
). - Keyboard navigation (up/down arrow keys for selection, Enter for confirmation).
If you need these features, feel free to implement them yourself or leave a comment below.
Source Code for this Chapter: https://github.com/84378996/simple_sdl_gui_tutorials/tree/main/tutorials09
Demo Video: