Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 50

Integrating ImGui with Your Graphics Application

Recommended resources

Hello! Welcome back to our project.

In our last lesson, we successfully initialized an OpenGL context using GLAD. You now have a C++ application that opens a window and clears it to a solid color, giving us a blank canvas. We have the foundational boilerplate, and now it's time to start painting our user interface.

Today, we will integrate the Dear ImGui library with the window and graphics context you've already created. This is a major step toward building the visual frontend for our telemetry engine. By the end of this lesson, you will render your first interactive UI element inside your application window.


What is Dear ImGui?

Dear ImGui (often just called ImGui) is a GUI library designed for creating tools, debuggers, and visualizations in real-time rendering applications. It's exceptionally well-suited for our project for a few key reasons:

  • Immediate Mode vs. Retained Mode: Unlike traditional UI toolkits (like Qt or WinForms) that maintain a complex state graph of UI objects (retained mode), ImGui operates on an "immediate mode" philosophy. You describe the UI you want to see for the current frame from scratch, every frame. This stateless approach makes it simple to use, highly flexible, and less prone to complex state management bugs, which is ideal for a dynamic tool like a profiler.
  • Performance: It's written in C++ with performance as a priority. It generates simple vertex buffers that are rendered by your application's graphics pipeline (OpenGL in our case), adding very little overhead. This is crucial for our goal of building a "zero-overhead" telemetry system.
  • Self-Contained: It has no external dependencies outside of your windowing and graphics backend, which we already have (GLFW and OpenGL).

ImGui is the industry standard for in-game and real-time application developer tools.

Dear ImGui Demo Application Screenshot
This is a screenshot of the official Dear ImGui demo application. It showcases the wide variety of widgets and windows you can create, from simple text and buttons to complex plots and logsโ€”all things we will eventually build.

Let's get it running in our project.


Step 1: Getting the ImGui Source Code

Like GLAD, we will add the ImGui source files directly to our project. The official documentation recommends this approach over trying to build it as a separate library.

  1. Go to the Dear ImGui GitHub repository.
  2. Click on the "releases" tab and download the source code .zip for the latest version (e.g., v1.90.8).
  3. Extract the zip file to a known location.

Step 2: Adding ImGui Files to Your Project

Now, we need to copy the relevant files from the extracted folder into our project. We need two sets of files: the core ImGui files and the "backend" implementation files for GLFW and OpenGL 3.

Create a new vendor directory in your project root, if you don't have one. Inside it, create an imgui directory. This is where we'll place the files.

Your project structure should look like this:

YourProject/
โ”œโ”€โ”€ CMakeLists.txt
โ”œโ”€โ”€ main.cpp
โ”œโ”€โ”€ src/
โ”‚   โ””โ”€โ”€ glad.c
โ”œโ”€โ”€ include/
โ”‚   โ”œโ”€โ”€ glad/
โ”‚   โ””โ”€โ”€ KHR/
โ””โ”€โ”€ vendor/      <-- NEW
    โ””โ”€โ”€ imgui/   <-- NEW

Now, copy the following files from the unzipped ImGui folder into your new vendor/imgui directory:

  • Core files (from the root of the ImGui folder):
    • imconfig.h
    • imgui.h
    • imgui.cpp
    • imgui_draw.cpp
    • imgui_internal.h
    • imgui_tables.cpp
    • imgui_widgets.cpp
    • imstb_rectpack.h
    • imstb_textedit.h
    • imstb_truetype.h
  • Backend files (from the backends/ directory inside the ImGui folder):
    • imgui_impl_glfw.h
    • imgui_impl_glfw.cpp
    • imgui_impl_opengl3.h
    • imgui_impl_opengl3.cpp

The following video from The Cherno provides a clear walkthrough of this file identification and copying process.

ImGui in OpenGL

Watch this short clip to see exactly which files to grab from the ImGui source and how to organize them within your project directory. It covers both the core files and the specific backend files we need for GLFW and OpenGL.

Watch from 2:06 to 4:12. Focus on the process of identifying and copying the necessary ImGui files into a vendor folder. You can ignore the parts about deleting the main.cpp from the example.


Step 3: Updating the Build System (CMake)

Next, we need to tell CMake about these new files so they get compiled and included in our final executable.

Open your CMakeLists.txt and make two changes:

  1. Add the new ImGui .cpp files to your add_executable command.
  2. Add the vendor/imgui directory to your target_include_directories.

Your CMakeLists.txt should now look like this:

cmake_minimum_required(VERSION 3.15)
project(TelemetryEngine)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Fetch GLFW
include(FetchContent)
FetchContent_Declare(
  glfw
  GIT_REPOSITORY https://github.com/glfw/glfw.git
  GIT_TAG        3.3.8
)
FetchContent_MakeAvailable(glfw)

# Our executable
add_executable(
    TelemetryEngine 
    main.cpp
    src/glad.c
    # --- Add ImGui source files ---
    vendor/imgui/imgui.cpp
    vendor/imgui/imgui_draw.cpp
    vendor/imgui/imgui_tables.cpp
    vendor/imgui/imgui_widgets.cpp
    vendor/imgui/imgui_impl_glfw.cpp
    vendor/imgui/imgui_impl_opengl3.cpp
)

# Link against GLFW
target_link_libraries(TelemetryEngine PRIVATE glfw)

# Tell our target where to find headers
target_include_directories(
    TelemetryEngine 
    PRIVATE 
    ${CMAKE_CURRENT_SOURCE_DIR}/include
    ${CMAKE_CURRENT_SOURCE_DIR}/vendor/imgui # <-- Add this line
)

# Prevent GLFW from including its own GL headers
target_compile_definitions(TelemetryEngine PRIVATE GLFW_INCLUDE_NONE)

After saving this file, regenerate your build files using CMake. Your project is now ready for the C++ integration.


Step 4: Integrating ImGui into Your Code

The process of integrating ImGui follows a standard lifecycle: Initialize, Create a New Frame, Render UI Widgets, Render the Frame, and Shutdown. The official ImGui wiki provides an excellent example for GLFW and OpenGL, which we will follow.

Getting Started ยท ocornut/imgui Wiki

This is the official 'Getting Started' guide from the ImGui wiki. It's the most authoritative source. We will follow the example specifically for GLFW and OpenGL.

Find the section titled 'Example: If you are using GLFW + OpenGL/WebGL'. Skim through the code snippets for 'Includes', 'Initialization', 'start of main loop', 'end of main loop', and 'Shutdown'. This is the exact pattern we will implement.

Let's apply this pattern to your main.cpp.

1. Includes

First, include the necessary ImGui headers at the top of main.cpp, right after your other includes.

#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>

// --- New ImGui Includes ---
#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"

2. Initialization

After you've created your GLFW window and initialized GLAD, you need to initialize ImGui. This sets up the ImGui context and configures the backends to work with your window and renderer.

// ... after gladLoadGLLoader ...

// --- Setup Dear ImGui context ---
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO(); (void)io;
// io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
// io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;  // Enable Gamepad Controls

// --- Setup Dear ImGui style ---
ImGui::StyleColorsDark();
//ImGui::StyleColorsClassic();

// --- Setup Platform/Renderer backends ---
const char* glsl_version = "#version 330";
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version);

3. The Render Loop

Your main while loop needs to be updated. The sequence is critical:

  1. Start a new ImGui frame.
  2. Define the UI elements you want to draw for this frame (e.g., windows, buttons).
  3. Render the ImGui draw data.
// Inside the Render Loop
while (!glfwWindowShouldClose(window)) {
    // Poll events
    glfwPollEvents();

    // --- Start the Dear ImGui frame ---
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();

    // --- UI Code Here ---
    // Example: Show a simple window
    ImGui::Begin("Hello, world!");
    ImGui::Text("This is some useful text.");
    ImGui::End();

    // --- Rendering ---
    glClearColor(0.1f, 0.1f, 0.15f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // Render ImGui draw data
    ImGui::Render();
    ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

    // Swap buffers
    glfwSwapBuffers(window);
}

Notice how ImGui rendering happens after glClear but before glfwSwapBuffers. ImGui draws its UI on top of whatever you have already rendered.

4. Shutdown

Finally, when your application exits, you need to clean up the ImGui resources. Add these calls just before glfwDestroyWindow.

// --- Cleanup ---
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplGlfw_Shutdown();
ImGui::DestroyContext();

glfwDestroyWindow(window);
glfwTerminate();

The video below walks through this exact sequence of Init, NewFrame, Render, and Shutdown calls.

ImGui in OpenGL

This clip reinforces the C++ integration steps. It shows where to place the initialization, main loop, and shutdown calls within your existing code.

Watch from 5:33 to 7:38. The presenter adds the ImGui initialization calls after creating the window, then integrates the NewFrame/Render calls into the main loop, and finally adds the shutdown calls.


Complete Code and Verification

Here is the complete main.cpp with all the changes. Compile and run your project.

#include <iostream>
#include <glad/glad.h>
#include <GLFW/glfw3.h>

#include "imgui.h"
#include "imgui_impl_glfw.h"
#include "imgui_impl_opengl3.h"

int main() {
    // Initialize GLFW
    if (!glfwInit()) {
        std::cerr << "Failed to initialize GLFW" << std::endl;
        return -1;
    }

    // Set window hints for OpenGL 3.3 Core Profile
    const char* glsl_version = "#version 330";
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif

    // Create the window
    GLFWwindow* window = glfwCreateWindow(1280, 720, "Telemetry Engine", NULL, NULL);
    if (window == nullptr) {
        std::cerr << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1); // Enable vsync

    // Initialize GLAD
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
        std::cerr << "Failed to initialize GLAD" << std::endl;
        glfwTerminate();
        return -1;
    }

    // Setup Dear ImGui context
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO(); (void)io;
    
    // Setup Dear ImGui style
    ImGui::StyleColorsDark();

    // Setup Platform/Renderer backends
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init(glsl_version);

    // The Render Loop
    while (!glfwWindowShouldClose(window)) {
        glfwPollEvents();

        // Start the Dear ImGui frame
        ImGui_ImplOpenGL3_NewFrame();
        ImGui_ImplGlfw_NewFrame();
        ImGui::NewFrame();

        // --- Our UI code goes here ---
        {
            ImGui::Begin("Info");
            ImGui::Text("Hello from ImGui!");
            ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);
            ImGui::End();
        }

        // Rendering
        glClearColor(0.1f, 0.1f, 0.15f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        ImGui::Render();
        ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

        glfwSwapBuffers(window);
    }

    // Cleanup
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();

    glfwDestroyWindow(window);
    glfwTerminate();

    return 0;
}

If everything is correct, you should see your window with a small, movable "Info" panel inside it displaying some text and your application's frame rate. Congratulations!

Test your understanding!

Inside the ImGui::Begin("Info"); ... ImGui::End(); block, add a button. If the button is clicked, it should print "Button Pressed!" to the standard console (std::cout).

Hint: Look up ImGui::Button() in the ImGui documentation or examples. It returns true when clicked.

Show answer
// Inside the "Our UI code goes here" block
{
    ImGui::Begin("Info");
    ImGui::Text("Hello from ImGui!");
    ImGui::Text("Application average %.3f ms/frame (%.1f FPS)", 1000.0f / ImGui::GetIO().Framerate, ImGui::GetIO().Framerate);

    // Add this code for the button
    if (ImGui::Button("Click Me")) {
        std::cout << "Button Pressed!" << std::endl;
    }
    
    ImGui::End();
}


Conclusion

Fantastic work today. You've taken a significant step by integrating a powerful UI library into our application. This forms the backbone of the entire visualizer suite we aim to build.

Key Takeaways:

  • ImGui uses an "immediate mode" paradigm, where the UI is rebuilt from code every frame.
  • Integrating ImGui involves adding its core source files plus backend implementations for your specific platform (GLFW) and renderer (OpenGL).
  • The ImGui lifecycle within the main loop is strictly ordered: NewFrame calls -> UI widget code -> Render -> RenderDrawData.
  • Initialization and Shutdown calls must properly wrap the main loop to manage the ImGui context.

What's Next?

Now that ImGui is up and running, our next step is to learn how to use its rich set of features effectively. In the next lesson, we will focus on creating a basic window with simple ImGui widgets, such as text inputs, sliders, and buttons, to start building out the interactive controls for our telemetry engine.