How to Create Multi-Column Layouts with Sorting and Resizing Using ImGui Tables
To create multi-column layouts with sorting and resizing in ImGui, call ImGui::BeginTable() with ImGuiTableFlags_Resizable and ImGuiTableFlags_Sortable, declare columns using ImGui::TableSetupColumn(), emit headers with ImGui::TableHeadersRow(), and retrieve sort specifications via ImGui::TableGetSortSpecs() to reorder your underlying data.
The Dear ImGui library (ocornut/imgui) implements a self-contained Tables API in imgui_tables.cpp that supports complex multi-column layouts with sorting and resizing through a persistent ImGuiTable instance stored in the global context. The public interface lives in imgui.h, providing everything needed to create interactive data grids without external dependencies.
Understanding the Tables API Architecture
ImGui handles tables through a single module architecture. The heavy lifting occurs in imgui_tables.cpp, while the public-facing functions are declared in imgui.h. When you create a table, the system allocates (or reuses) an ImGuiTable instance stored in GImGui, which persists across frames to maintain state such as column widths, display order, and sort specifications without requiring manual storage in your application code.
Essential Flags for Resizable and Sortable Tables
The table behavior is controlled by flags passed to BeginTable():
ImGuiTableFlags_Resizable– Enables dragging column borders to resize. Internally, resize requests are queued and applied viaTableApplyQueuedRequests()around lines 78-90 inimgui_tables.cpp, which callsTableSetColumnWidth()to updateWidthAutoand recalculate stretch weights.ImGuiTableFlags_Sortable– Allows clicking headers to toggle sort order. Click handling occurs inTableSortSpecsClickColumn()(around lines 58-60), which updates theImGuiTableSortSpecarray and marks the table for resorting.ImGuiTableFlags_Reorderable– Permits dragging headers to change column order. The request is stored inTableQueueSetColumnDisplayOrder()and applied later byTableSetColumnDisplayOrder()(lines 777-785).ImGuiTableFlags_ScrollX / ScrollY– Creates an internal scrolling child window withinBeginTableEx()(lines 40-46), essential when total column width exceeds the available outer size.
Step-by-Step Table Creation Flow
Step 1: Initialize with BeginTable
Call ImGui::BeginTable(const char* id, int columns, ImGuiTableFlags flags, ImVec2 outer_size = ImVec2(0,0), float inner_width = 0) to allocate the table instance and determine sizing policies through TableFixFlags(). An outer size of (0,0) allows the table to fill remaining window space.
Step 2: Declare Columns with TableSetupColumn
Define each column using ImGui::TableSetupColumn(const char* label, ImGuiTableColumnFlags flags = 0, float init_width_or_weight = 0, ImGuiID user_data = 0). This must be called before the first row (enforced by an assertion in the implementation) to store per-column metadata including width policy (WidthFixed or WidthStretch) and optional user IDs for sorting.
Step 3: Emit Headers with TableHeadersRow
Invoke ImGui::TableHeadersRow() to iterate over declared columns and render header cells. This function automatically registers click and resize interactions. When ImGuiTableFlags_Sortable is active, left-clicks trigger TableSortSpecsClickColumn() to update sort direction.
Step 4: Fill Rows and Cells
Use ImGui::TableNextRow() to create a new row and ImGui::TableNextColumn() to advance to the next cell. Row height is computed from the tallest visible cell, ensuring consistent alignment even with hidden columns.
Step 5: Finalize with EndTable
Call ImGui::EndTable() to complete drawing, merge draw channels, and persist settings to the .ini file if enabled.
Implementing Sort Logic
After TableHeadersRow(), retrieve the current sort specifications:
const ImGuiTableSortSpecs* sorts = ImGui::TableGetSortSpecs();
if (sorts && sorts->SpecsCount > 0)
{
for (int n = 0; n < sorts->SpecsCount; n++)
{
const ImGuiTableSortSpec* spec = &sorts->Specs[n];
// spec->ColumnIndex, spec->SortDirection, spec->ColumnUserID
}
}
The ImGuiTableSortSpec struct contains ColumnIndex, SortOrder (for multi-column sorts), SortDirection (ascending/descending), and ColumnUserID (if specified in TableSetupColumn()). Your application reads this data and reorders its data source accordingly.
Managing Resizing and Reordering Internally
When a user drags a column border, ImGui records ResizedColumn and ResizedColumnNextWidth. On the subsequent frame, TableApplyQueuedRequests() (called from BeginTableEx) invokes TableSetColumnWidth() to apply the new width. For reordering, drag operations queue via TableQueueSetColumnDisplayOrder() and are processed by TableSetColumnDisplayOrder() to update the display indices persistently.
Complete Implementation Examples
Example 1: Basic Resizable, Sortable, and Reorderable Table
if (ImGui::BeginTable("my_table", 3,
ImGuiTableFlags_Resizable |
ImGuiTableFlags_Sortable |
ImGuiTableFlags_Reorderable |
ImGuiTableFlags_Borders |
ImGuiTableFlags_RowBg))
{
// Define columns: Fixed width for ID, stretch for others
ImGui::TableSetupColumn("ID", ImGuiTableColumnFlags_WidthFixed, 80.0f);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Score", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
struct Item { int id; const char* name; int score; };
static Item items[] = { {1,"Alice", 95}, {2,"Bob", 78}, {3,"Carol", 88} };
// Handle sorting
const ImGuiTableSortSpecs* sorts = ImGui::TableGetSortSpecs();
if (sorts && sorts->SpecsCount > 0)
{
const ImGuiTableSortSpec* spec = sorts->Specs;
std::stable_sort(std::begin(items), std::end(items),
[spec](const Item& a, const Item& b)
{
if (spec->ColumnIndex == 0)
return (spec->SortDirection == ImGuiSortDirection_Ascending) ?
a.id < b.id : a.id > b.id;
else if (spec->ColumnIndex == 2)
return (spec->SortDirection == ImGuiSortDirection_Ascending) ?
a.score < b.score : a.score > b.score;
return false;
});
}
// Populate rows
for (const Item& it : items)
{
ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::Text("%d", it.id);
ImGui::TableNextColumn(); ImGui::TextUnformatted(it.name);
ImGui::TableNextColumn(); ImGui::Text("%d", it.score);
}
ImGui::EndTable();
}
Example 2: Scrollable Table with Frozen Columns
if (ImGui::BeginTable("scrollable_table", 5,
ImGuiTableFlags_Resizable |
ImGuiTableFlags_Sortable |
ImGuiTableFlags_ScrollX |
ImGuiTableFlags_ScrollY |
ImGuiTableFlags_FreezeFirstColumn |
ImGuiTableFlags_Borders))
{
// Freeze first 2 columns horizontally
ImGui::TableSetupScrollFreeze(2, 0);
ImGui::TableSetupColumn("Idx", ImGuiTableColumnFlags_WidthFixed, 50);
ImGui::TableSetupColumn("Name", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Age", ImGuiTableColumnFlags_WidthFixed, 40);
ImGui::TableSetupColumn("Country", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn("Score", ImGuiTableColumnFlags_WidthFixed, 60);
ImGui::TableHeadersRow();
for (int n = 0; n < 200; n++)
{
ImGui::TableNextRow();
ImGui::TableNextColumn(); ImGui::Text("%d", n);
ImGui::TableNextColumn(); ImGui::Text("Player %d", n);
ImGui::TableNextColumn(); ImGui::Text("%d", 20 + n % 30);
ImGui::TableNextColumn(); ImGui::Text("Country %d", n % 5);
ImGui::TableNextColumn(); ImGui::Text("%d", rand() % 100);
}
ImGui::EndTable();
}
Example 3: Sorting with Custom User IDs
if (ImGui::BeginTable("custom_id_table", 2,
ImGuiTableFlags_Sortable | ImGuiTableFlags_Resizable))
{
// Assign stable IDs to identify columns by logical name rather than index
ImGui::TableSetupColumn("File", ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)1);
ImGui::TableSetupColumn("Size (KB)", ImGuiTableColumnFlags_None, 0.0f, (ImGuiID)2);
ImGui::TableHeadersRow();
const ImGuiTableSortSpecs* sort = ImGui::TableGetSortSpecs();
if (sort && sort->SpecsCount > 0)
{
const ImGuiTableSortSpec* spec = sort->Specs;
// Use spec->ColumnUserID (1 or 2) to determine sort logic
// rather than relying on spec->ColumnIndex
}
// ... fill rows ...
ImGui::EndTable();
}
Key Source Files for Reference
| File | Purpose | Location |
|---|---|---|
imgui.h |
Public API declarations for BeginTable, TableSetupColumn, TableHeadersRow, and TableGetSortSpecs |
imgui.h |
imgui_tables.cpp |
Core implementation including TableApplyQueuedRequests, TableSortSpecsClickColumn, and TableSetColumnDisplayOrder |
imgui_tables.cpp |
imgui_demo.cpp |
Real-world usage examples demonstrating column setup, resizing policies, and sorting implementations | imgui_demo.cpp |
Summary
- Use
ImGuiTableFlags_ResizableandImGuiTableFlags_SortableinBeginTable()to enable interactive column features. - Call
TableSetupColumn()before the first row to define width policies (WidthFixedvsWidthStretch) and optional user IDs. - Invoke
TableHeadersRow()to generate clickable headers that automatically handle sort direction toggles and resize interactions. - Access
ImGui::TableGetSortSpecs()after drawing headers to retrieve an array ofImGuiTableSortSpeccontainingColumnIndex,SortDirection, andColumnUserID. - Resize and reorder requests are queued internally in
TableApplyQueuedRequests()and applied on the next frame via functions likeTableSetColumnWidth()andTableSetColumnDisplayOrder(). - The
ImGuiTableinstance persists across frames in the global context, automatically preserving column widths, order, and sort state.
Frequently Asked Questions
How do I disable resizing for specific columns only?
Pass ImGuiTableColumnFlags_NoResize to TableSetupColumn() for that specific column while keeping ImGuiTableFlags_Resizable on the table. This allows most columns to be resized while freezing the width of critical columns.
Can I sort by multiple columns simultaneously?
Yes. When multiple sort criteria are active, TableGetSortSpecs() returns an array where SpecsCount indicates how many columns are involved in the sort. Iterate through all ImGuiTableSortSpec entries, each providing ColumnIndex, SortOrder (priority), and SortDirection to implement multi-level sorting in your comparison function.
How do I persist column widths between application restarts?
ImGui automatically saves table settings including column widths to the .ini file when ImGuiTableFlags_SaveSettings is enabled (default in most configurations), provided io.IniFilename is set. The settings are loaded automatically on startup and saved when the context shuts down or when SaveIniSettingsToDisk() is called.
Why must TableSetupColumn be called before the first row?
The implementation in imgui_tables.cpp enforces this via an assertion because column metadata must be fully initialized before BeginTableEx() calculates the initial layout and before TableHeadersRow() iterates over the column array to render headers and register interaction hit boxes.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →