Skip to main content

Welcome to a Grasp lesson

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

Lesson 55

Custom Drawing with ImGui's ImDrawList

Recommended resources

Hello! Welcome back to our work on the Telemetry Engine.

In the previous lesson, we successfully displayed real-time data using ImGui's built-in PlotLines widget. We created a scrolling graph, which is a great first step. However, we also identified its critical limitation: PlotLines iterates over every single data point to render the plot. For the high-frequency data our engine will capture, this would be a major performance bottleneck, directly causing the "observer effect" we aim to eliminate.

To build a truly high-performance plotter, we need to move beyond standard widgets and take direct control of the rendering pipeline. This lesson is the key to unlocking that control. We will dive into ImGui's low-level drawing API, ImDrawList. You'll learn how to get the draw list for a window and use it to render custom primitives like lines, rectangles, and text directly to the screen.

Mastering the ImDrawList is the foundational skill for building any custom widget in ImGui and is absolutely essential for creating our efficient LOD Plotter.

What is the Draw List?

Every widget you've used so far—ImGui::Button, ImGui::Text, and even ImGui::PlotLines—is ultimately a high-level helper function. Under the hood, these functions all do the same thing: they add vertices, indices, and drawing commands to a special object called an ImDrawList.

Think of an ImDrawList as a recipe for the GPU. It's a list of simple instructions: "draw these triangles here," "draw those triangles there," "change the texture," "clip rendering to this rectangle." At the end of each frame, ImGui gathers all the draw lists from all your windows and sends them to your graphics backend (like OpenGL) to be rendered.

Each ImGui window has its own draw list. By accessing it directly, we can bypass the high-level widgets and add our own custom drawing commands to the queue. This gives us pixel-perfect control over the final output.

ImGui Custom Rendering Example
This image demonstrates what's possible with the draw list API—from simple primitives to an interactive drawing canvas. This level of control is what we'll be harnessing.

Accessing the Draw List

The first step is to get a pointer to the draw list of the window you want to draw in. You do this with the function ImGui::GetWindowDrawList().

It's crucial to call this after ImGui::Begin() and before ImGui::End(), as it returns the draw list for the "current" active window.

// Inside your main loop or an update() function
ImGui::Begin("My Custom Canvas");

// Get the draw list for this window
ImDrawList* draw_list = ImGui::GetWindowDrawList();

// ... We will add drawing commands here ...

ImGui::End();

Let's see a practical demonstration of this.

ImGui C++ Course 2025 – Complete Beginner to Advanced Guide (C++ UI Design Tutorial)

The following video shows how to get the draw list for a window and use it to render a custom shape. This is the core workflow we'll be using.

Watch from 01:03:01 to 01:08:08. Pay close attention to the sequence of operations: calling ImGui::GetWindowDrawList(), getting the current screen position, and then using the draw_list pointer to call a function like AddRectFilledMultiColor() with specific coordinates and colors.


Drawing Primitives

As you saw in the video, once you have the ImDrawList*, you can call its member functions to add shapes. Let's break down the most common ones.

Coordinates and Colors

Before we draw, we need to understand two things:

  1. Coordinates (ImVec2): All drawing functions use screen-space pixel coordinates. The origin (0,0) is the top-left corner of the window's content area. An ImVec2 is a simple struct with float x and float y members. You can get the top-left corner of the current window's content area using ImGui::GetWindowPos() + ImGui::GetWindowContentRegionMin().

  2. Colors (ImU32): Colors are passed as a 32-bit unsigned integer (ImU32) where each byte represents a channel (RGBA). The easiest way to create this value is with the IM_COL32(R, G, B, A) macro.

    // An opaque yellow color
    ImU32 my_color = IM_COL32(255, 255, 0, 255);
    

Drawing Lines: AddLine()

This is the most fundamental primitive for our plot.

void AddLine(const ImVec2& p1, const ImVec2& p2, ImU32 col, float thickness = 1.0f);
  • p1, p2: The start and end points of the line.
  • col: The color of the line.
  • thickness: The thickness of the line in pixels.

Drawing Rectangles: AddRect() and AddRectFilled()

Essential for drawing plot areas, bars, and other UI elements.

// Draws an outline of a rectangle
void AddRect(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawCornerFlags corner_flags = ImDrawCornerFlags_All, float thickness = 1.0f);

// Draws a solid, filled rectangle
void AddRectFilled(const ImVec2& p_min, const ImVec2& p_max, ImU32 col, float rounding = 0.0f, ImDrawCornerFlags corner_flags = ImDrawCornerFlags_All);
  • p_min: The top-left corner of the rectangle.
  • p_max: The bottom-right corner of the rectangle.
  • col: The color.
  • rounding: Optional radius for rounded corners.

Drawing Text: AddText()

Useful for adding labels, axes, or tooltips to our plot.

void AddText(const ImVec2& pos, ImU32 col, const char* text_begin, const char* text_end = NULL);
  • pos: The top-left corner of the text's bounding box.
  • col: The color of the text.
  • text_begin: A pointer to the start of the text string.

These are just the basics. The Draw List API supports circles, triangles, bezier curves, and more.

imgui.h - Primitives

We've covered the most common shapes, but the Draw List API is much richer. The official imgui.h header file is the best place to discover all available drawing functions.

Skim through the 'Primitives' section in this header file. You don't need to memorize everything, but notice the variety of shapes you can draw: AddCircle, AddTriangle, AddPolyline, AddBezierCurve, etc. This will be your go-to reference when you need to draw something specific.


Putting it all Together: A Custom Canvas

Let's apply these concepts to create a simple drawing canvas. This example demonstrates how to get the window boundaries, handle mouse input, and draw primitives. You can add this to your MyApp::update() method.

void MyApp::update()
{
    ImGui::Begin("My Custom Canvas");

    // Get a pointer to the current window's draw list
    ImDrawList* draw_list = ImGui::GetWindowDrawList();
    
    // Get the current window position and size
    ImVec2 canvas_pos = ImGui::GetCursorScreenPos(); // Top-left of the window content area
    ImVec2 canvas_size = ImGui::GetContentRegionAvail(); // Size of the content area
    if (canvas_size.x < 50.0f) canvas_size.x = 50.0f; // Clamp size
    if (canvas_size.y < 50.0f) canvas_size.y = 50.0f;
    ImVec2 canvas_p1 = canvas_pos;
    ImVec2 canvas_p2 = ImVec2(canvas_pos.x + canvas_size.x, canvas_pos.y + canvas_size.y);

    // 1. Draw the canvas background and border
    ImGuiIO& io = ImGui::GetIO();
    draw_list->AddRectFilled(canvas_p1, canvas_p2, IM_COL32(50, 50, 50, 255));
    draw_list->AddRect(canvas_p1, canvas_p2, IM_COL32(255, 255, 255, 255));

    // This is to "reserve" the space for our canvas, so other widgets don't draw on top
    ImGui::InvisibleButton("canvas", canvas_size);

    // 2. Draw a cross in the center of the canvas
    ImVec2 center = ImVec2(canvas_p1.x + canvas_size.x * 0.5f, canvas_p1.y + canvas_size.y * 0.5f);
    ImU32 cross_color = IM_COL32(255, 255, 0, 255);
    draw_list->AddLine(ImVec2(center.x - 10, center.y), ImVec2(center.x + 10, center.y), cross_color, 2.0f);
    draw_list->AddLine(ImVec2(center.x, center.y - 10), ImVec2(center.x, center.y + 10), cross_color, 2.0f);

    // 3. Draw a circle that follows the mouse cursor, but only when inside the canvas
    if (ImGui::IsItemHovered()) // Is the InvisibleButton hovered?
    {
        ImVec2 mouse_pos_in_canvas = io.MousePos;
        draw_list->AddCircleFilled(mouse_pos_in_canvas, 10.0f, IM_COL32(0, 255, 0, 255), 12);
        ImGui::SetTooltip("Mouse at (%.1f, %.1f)", mouse_pos_in_canvas.x - canvas_p1.x, mouse_pos_in_canvas.y - canvas_p1.y);
    }
    
    // 4. Add some text
    draw_list->AddText(ImVec2(canvas_p1.x + 5, canvas_p1.y + 5), IM_COL32(200, 200, 200, 255), "Custom Drawing Area");

    ImGui::End();
}

Run this code and experiment with it. Move your mouse over the canvas and resize the window. See how the primitives are redrawn correctly each frame based on the calculated coordinates.

Test your understanding!

Modify the example above to draw a horizontal progress bar at the bottom of the canvas. Add a slider widget (ImGui::SliderFloat) outside the canvas that controls the progress (from 0.0 to 1.0). The progress bar should be a filled rectangle that grows from left to right as the slider value increases.

Show answer

First, you'd add a SliderFloat to control a static variable. Then, you'd use that variable to calculate the width of the filled rectangle for the progress bar.

// Inside MyApp::update()

// Add a slider to control the progress
static float progress = 0.5f;
ImGui::SliderFloat("Progress", &progress, 0.0f, 1.0f);

ImGui::Begin("My Custom Canvas");

// ... (previous canvas setup code) ...
ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 canvas_p1 = ImGui::GetCursorScreenPos();
ImVec2 canvas_size = ImGui::GetContentRegionAvail();
// ... (rest of canvas setup) ...

// --- Add this code to draw the progress bar ---

// Define the progress bar's geometry
float bar_height = 20.0f;
ImVec2 bar_p1 = ImVec2(canvas_p1.x, canvas_p1.y + canvas_size.y - bar_height);
ImVec2 bar_p2_filled = ImVec2(bar_p1.x + canvas_size.x * progress, bar_p1.y + bar_height);
ImVec2 bar_p2_full = ImVec2(bar_p1.x + canvas_size.x, bar_p1.y + bar_height);

// Draw the background of the bar
draw_list->AddRectFilled(bar_p1, bar_p2_full, IM_COL32(100, 100, 100, 255));
// Draw the filled portion based on progress
draw_list->AddRectFilled(bar_p1, bar_p2_filled, IM_COL32(100, 200, 100, 255));

ImGui::End();

Conclusion

You have now gained one of the most powerful skills for working with ImGui: direct rendering via the ImDrawList. This frees you from the constraints of built-in widgets and allows you to create any visualization you can imagine, which is exactly what your high-performance telemetry engine requires.

Key Takeaways:

  • The ImDrawList is the low-level API for adding drawing commands (vertices and indices) to an ImGui window.
  • You can get a window's draw list using ImGui::GetWindowDrawList() inside a Begin/End block.
  • Drawing coordinates are in screen-space pixels, with (0,0) at the top-left of the content area.
  • Colors are packed into a ImU32 integer, easily created with the IM_COL32() macro.
  • The API provides functions like AddLine, AddRectFilled, and AddText to build up complex visuals from simple primitives.

What's Next?

We can now draw whatever we want, wherever we want on the screen. The next challenge is connecting this to our actual telemetry data. A data point in our system might be (timestamp, value), but AddLine needs (x, y) pixel coordinates.

In the next lesson, we will bridge this gap. You will learn to implement a coordinate transformation function that maps data values from their own space (e.g., time, FPS) into the screen-space pixels required by the ImDrawList API. This is the mathematical heart of our custom plotter.