본문으로 건너뛰기
Previous
Next
Modern C++ GUI: Debug Tools & Dashboards with Dear ImGui

Modern C++ GUI: Debug Tools & Dashboards with Dear ImGui

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

MistakeSymptomFix
State in a stack localSlider jumps back every frameUse static local, member var, or global
Wrong NewFrame/Render orderBlank window or crashAlways: backend NewFrame → ImGui::NewFrame → widgets → Render → RenderDrawData
Missing backend initBlack screen, no inputCall ImGui_ImplGlfw_Init and ImGui_ImplOpenGL3_Init before the loop
Duplicate widget labelsButton only works once / wrong targetUse PushID/PopID or ##suffix
Calling ImGui from worker threadRandom crashesRoute all ImGui calls to render thread
Forgetting ImGui::End()Assertion failureEvery 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/PopID around 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 tearing
  • CollapsingHeader and BeginTable keep complex dashboards organized
  • Release builds: wrap all ImGui code in #ifdef to 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, Dear ImGui, ImGui, Game Dev, Tools, Debugging 등으로 검색하시면 이 글이 도움이 됩니다.