Modern C++ GUI: Debug Tools & Dashboards with Dear ImGui
이 글의 핵심
Dear ImGui immediate-mode GUI in C++: GLFW/OpenGL backend setup, common widgets, PlotLines for live graphs, debug dashboards, threading rules, and production tips.
Why Dear ImGui?
Printf-based debugging works until your program runs at 60 frames per second and you need to watch 20 variables simultaneously while tweaking constants without recompiling. Dear ImGui solves this: it is a C++ immediate-mode GUI library that is trivial to drop into an existing OpenGL/Vulkan/DirectX render loop and gives you sliders, graphs, tables, and log windows in real time.
It is used in game engines (Unreal Editor uses it for profiling tools), simulation software, robot-control GUIs, and virtually any situation where a developer wants a visual interface without committing to a full UI framework.
Immediate mode is the key concept. Unlike retained-mode toolkits (Qt, wxWidgets) that build a widget object tree and fire callbacks, ImGui rebuilds the UI from scratch every frame:
// Every frame — no widget objects survive between calls
if (ImGui::Button("Reset"))
simulation.reset(); // called only when clicked
ImGui::SliderFloat("Speed", &speed, 0.0f, 100.0f);
// 'speed' is YOUR variable — ImGui reads and writes it in place
There are no signals, no XML layout files, no style sheets to maintain. The result is extremely low API surface — you learn 20 functions and can build almost anything.
Project Structure
Dear ImGui is a header+source drop-in. The minimal set of files:
your-project/
├── imgui/ ← copy from the Dear ImGui repo
│ ├── imgui.h
│ ├── imgui.cpp
│ ├── imgui_draw.cpp
│ ├── imgui_tables.cpp
│ ├── imgui_widgets.cpp
│ ├── backends/
│ │ ├── imgui_impl_glfw.h
│ │ ├── imgui_impl_glfw.cpp
│ │ ├── imgui_impl_opengl3.h
│ │ └── imgui_impl_opengl3.cpp
├── main.cpp
└── CMakeLists.txt
A minimal CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(imgui_demo)
find_package(OpenGL REQUIRED)
find_package(glfw3 REQUIRED)
add_executable(demo
main.cpp
imgui/imgui.cpp
imgui/imgui_draw.cpp
imgui/imgui_tables.cpp
imgui/imgui_widgets.cpp
imgui/backends/imgui_impl_glfw.cpp
imgui/backends/imgui_impl_opengl3.cpp
)
target_include_directories(demo PRIVATE imgui imgui/backends)
target_link_libraries(demo PRIVATE OpenGL::GL glfw)
The Frame Order
Every ImGui frame follows a strict four-step sequence. Getting this wrong causes blank windows or crashes:
1. Backend::NewFrame() ← poll input, compute delta-time
2. ImGui::NewFrame() ← start building the UI
3. ImGui::Begin() ... ImGui::End() ← define windows and widgets
4. ImGui::Render() ← emit draw commands
5. Backend::RenderDrawData() ← hand draw list to GPU
In code:
// 1 & 2 — start of frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// 3 — your UI code
ImGui::Begin("Debug");
ImGui::Text("Frame time: %.3f ms", 1000.0f / ImGui::GetIO().Framerate);
ImGui::End();
// 4 & 5 — render
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
Complete Minimal Example
A full working program with GLFW + OpenGL3 backend:
#include <GLFW/glfw3.h>
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"
#include <cstdio>
#include <vector>
int main() {
if (!glfwInit()) return 1;
// OpenGL 3.3 core profile
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(1280, 720, "Dear ImGui Demo", nullptr, nullptr);
if (!window) return 1;
glfwMakeContextCurrent(window);
glfwSwapInterval(1); // vsync
// ImGui setup
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
ImGui::StyleColorsDark();
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init("#version 330");
// Persistent state — MUST survive between frames
static float speed = 10.0f;
static bool show_debug = true;
static int counter = 0;
static std::vector<float> frame_times;
while (!glfwWindowShouldClose(window)) {
glfwPollEvents();
// --- ImGui frame start ---
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
// --- Your UI code ---
if (show_debug) {
ImGui::Begin("Control Panel", &show_debug);
ImGui::Text("FPS: %.1f", io.Framerate);
ImGui::Separator();
ImGui::SliderFloat("Speed", &speed, 0.0f, 100.0f);
ImGui::Text("Current speed: %.2f", speed);
if (ImGui::Button("Increment counter"))
++counter;
ImGui::SameLine();
ImGui::Text("Count: %d", counter);
// Live frame-time graph
frame_times.push_back(1000.0f / io.Framerate);
if (frame_times.size() > 120) frame_times.erase(frame_times.begin());
ImGui::PlotLines("Frame ms",
frame_times.data(),
static_cast<int>(frame_times.size()),
0, nullptr, 0.0f, 50.0f,
ImVec2(0, 80));
ImGui::End();
}
// --- Render ---
ImGui::Render();
int display_w, display_h;
glfwGetFramebufferSize(window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
glfwSwapBuffers(window);
}
// Cleanup
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();
glfwDestroyWindow(window);
glfwTerminate();
}
Common Widgets
Text, Buttons, and Input
ImGui::Text("Position: (%.2f, %.2f)", x, y); // formatted text
ImGui::TextColored(ImVec4(1,0,0,1), "ERROR: %s", msg); // colored text
if (ImGui::Button("Fire Missile")) // returns true on click
fireMissile();
static char buf[256] = "";
ImGui::InputText("Name", buf, sizeof(buf)); // text input field
static bool enabled = true;
ImGui::Checkbox("Enable lighting", &enabled); // checkbox
Sliders and Drag Controls
static float brightness = 1.0f;
ImGui::SliderFloat("Brightness", &brightness, 0.0f, 2.0f);
static int count = 10;
ImGui::SliderInt("Count", &count, 1, 100);
static float pos[3] = {0, 0, 0};
ImGui::DragFloat3("Position", pos, 0.1f); // drag to change, faster than slider
Color Picker
static float color[4] = {1.0f, 0.5f, 0.0f, 1.0f}; // RGBA
ImGui::ColorEdit4("Ambient color", color);
Collapsible Sections
if (ImGui::CollapsingHeader("Physics")) {
ImGui::SliderFloat("Gravity", &gravity, -20.0f, 0.0f);
ImGui::SliderFloat("Friction", &friction, 0.0f, 1.0f);
}
if (ImGui::CollapsingHeader("Rendering")) {
ImGui::Checkbox("Wireframe", &wireframe);
ImGui::Checkbox("Show normals", &show_normals);
}
Tables
if (ImGui::BeginTable("entities", 3,
ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable))
{
ImGui::TableSetupColumn("ID");
ImGui::TableSetupColumn("Position");
ImGui::TableSetupColumn("Health");
ImGui::TableHeadersRow();
for (const auto& e : entities) {
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0); ImGui::Text("%d", e.id);
ImGui::TableSetColumnIndex(1); ImGui::Text("(%.1f, %.1f)", e.x, e.y);
ImGui::TableSetColumnIndex(2); ImGui::Text("%.0f%%", e.health);
}
ImGui::EndTable();
}
Building a Live Dashboard
A server monitoring dashboard that reads from a shared stats struct:
// stats.h — updated by background thread, read by UI
struct ServerStats {
std::atomic<int> active_connections{0};
std::atomic<int> requests_per_second{0};
std::atomic<float> cpu_usage{0.0f};
std::atomic<float> memory_mb{0.0f};
static constexpr int HISTORY = 120;
// ring buffer updated under a mutex
std::array<float, HISTORY> cpu_history{};
std::array<float, HISTORY> rps_history{};
int history_offset = 0;
std::mutex history_mutex;
};
extern ServerStats g_stats;
// In the ImGui frame:
void renderDashboard() {
ImGui::SetNextWindowSize(ImVec2(500, 400), ImGuiCond_FirstUseEver);
ImGui::Begin("Server Dashboard");
// Summary row
ImGui::Text("Connections: %d", g_stats.active_connections.load());
ImGui::SameLine(200);
ImGui::Text("RPS: %d", g_stats.requests_per_second.load());
ImGui::Separator();
// CPU gauge
float cpu = g_stats.cpu_usage.load();
char overlay[32];
snprintf(overlay, sizeof(overlay), "CPU %.1f%%", cpu);
ImGui::ProgressBar(cpu / 100.0f, ImVec2(-1, 0), overlay);
// Memory
float mem = g_stats.memory_mb.load();
ImGui::Text("Memory: %.1f MB", mem);
// Historical graphs — copy under lock to avoid tearing
float cpu_snap[ServerStats::HISTORY];
float rps_snap[ServerStats::HISTORY];
int offset;
{
std::lock_guard lock(g_stats.history_mutex);
std::copy(g_stats.cpu_history.begin(), g_stats.cpu_history.end(), cpu_snap);
std::copy(g_stats.rps_history.begin(), g_stats.rps_history.end(), rps_snap);
offset = g_stats.history_offset;
}
ImGui::PlotLines("CPU %", cpu_snap, ServerStats::HISTORY, offset,
nullptr, 0.0f, 100.0f, ImVec2(0, 60));
ImGui::PlotLines("RPS", rps_snap, ServerStats::HISTORY, offset,
nullptr, 0.0f, 1000.0f, ImVec2(0, 60));
ImGui::End();
}
Avoiding ID Clashes
ImGui identifies widgets by the text label you pass. When you have two buttons both labeled “Delete”, ImGui sees them as the same widget:
// PROBLEM: two buttons with the same label → same ID → first one wins
for (auto& item : items) {
if (ImGui::Button("Delete")) deleteItem(item); // all fire item[0]!
}
// FIX 1: ##suffix is part of the ID but not displayed
for (int i = 0; i < items.size(); ++i) {
ImGui::PushID(i);
if (ImGui::Button("Delete")) deleteItem(items[i]);
ImGui::PopID();
}
// FIX 2: PushID/PopID with a unique value
for (const auto& item : items) {
ImGui::PushID(item.id);
if (ImGui::Button("Delete")) deleteItem(item);
ImGui::PopID();
}
// FIX 3: embed unique value in label with ## separator
ImGui::Button("Delete##42"); // displays "Delete", ID is "Delete##42"
Always use PushID/PopID around loops that create multiple widgets of the same type.
Threading Rules
Dear ImGui is not thread-safe. All calls must come from the rendering thread. The correct pattern:
// Worker thread — update shared state only
void workerThread(ServerStats& stats) {
while (running) {
stats.cpu_usage.store(measureCPU());
stats.active_connections.store(countConnections());
{
std::lock_guard lock(stats.history_mutex);
stats.cpu_history[stats.history_offset] = stats.cpu_usage.load();
stats.history_offset = (stats.history_offset + 1) % ServerStats::HISTORY;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
// Main/render thread — only place ImGui is called
void renderThread() {
while (!glfwWindowShouldClose(window)) {
// ... ImGui frame sequence ...
renderDashboard(); // reads g_stats, all atomics or under lock
}
}
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| State in a stack local | Slider jumps back every frame | Use static local, member var, or global |
| Wrong NewFrame/Render order | Blank window or crash | Always: backend NewFrame → ImGui::NewFrame → widgets → Render → RenderDrawData |
| Missing backend init | Black screen, no input | Call ImGui_ImplGlfw_Init and ImGui_ImplOpenGL3_Init before the loop |
| Duplicate widget labels | Button only works once / wrong target | Use PushID/PopID or ##suffix |
| Calling ImGui from worker thread | Random crashes | Route all ImGui calls to render thread |
Forgetting ImGui::End() | Assertion failure | Every Begin() needs a matching End() |
Production Tips
Conditional compilation: wrap the entire UI behind a macro so release builds include zero ImGui code:
#ifdef ENABLE_DEBUG_UI
renderDashboard();
#endif
Saving window layout: enable io.IniFilename to auto-save window positions to a file:
io.IniFilename = "debug_layout.ini"; // nullptr to disable
Docking: enable the docking branch for tabbable, dockable windows:
io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
// Then in each frame:
ImGui::DockSpaceOverViewport();
Fonts: load a custom font for better readability at small sizes:
ImGuiIO& io = ImGui::GetIO();
io.Fonts->AddFontFromFileTTF("fonts/Roboto-Regular.ttf", 14.0f);
// Must be called before the first frame
Key Takeaways
- Frame order is sacred: backend NewFrame → ImGui::NewFrame → widgets → ImGui::Render → backend RenderDrawData
- State must be persistent — local variables reset every frame; use
static, members, or globals for slider values PushID/PopIDaround loops to prevent ID collisions for repeated widget labels- Not thread-safe — call ImGui only from the render thread; use atomics/mutexes to share data with worker threads
PlotLines+ atomic stats = live graphs with minimal code; copy under lock to avoid tearingCollapsingHeaderandBeginTablekeep complex dashboards organized- Release builds: wrap all ImGui code in
#ifdefto compile it out for shipping
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Dear ImGui immediate-mode GUI in C++: GLFW/OpenGL backend, widgets, PlotLines, dashboards, threading rules, and producti… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 크로스 플랫폼 GUI | Qt 기초 완벽 가이드 [#36-2]
- C++ Python과 C++의 만남 | pybind11으로 고성능 엔진 만들기 [#35-1]
- C++ WebAssembly(Wasm)와 Emscripten | C++을 브라우저에서 돌리기 [#35-2]
이 글에서 다루는 키워드 (관련 검색어)
C++, GUI, Dear ImGui, ImGui, Game Dev, Tools, Debugging 등으로 검색하시면 이 글이 도움이 됩니다.