Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

For AI assistants

This page is written for AI assistants. Give it to yours: it will know how to help you write applications with Dear ImGui Bundle, in Python or C++. It is also at https://imgui-bundle.pages.dev/llms.txt.

About this page

Dear ImGui Bundle is a set of libraries on top of Dear ImGui, to write interactive applications in Python and C++: on the desktop, on mobile, and in the browser.

Read this page first. It links to more:

Install

Python (3.11 or later; binary wheels for Windows, macOS and Linux):

pip install imgui-bundle

C++: start from the template, https://github.com/pthom/imgui_bundle_template. In CMake, imgui_bundle_add_app(my_app my_app.cpp) creates an app, with its assets.

In the browser, with no install: the Python playground, https://imgui-bundle.pages.dev/playground/, runs Dear ImGui Bundle in Python (Pyodide), with ready-to-run demos.

The install pages, with the platform notes: https://imgui-bundle.pages.dev/doc/python/python-install/ and https://imgui-bundle.pages.dev/doc/cpp/cpp-install/

Not pyimgui

pyimgui (pip install imgui, then import imgui) is another Python binding of Dear ImGui, with another API. Its code does not run with Dear ImGui Bundle. Assistants often mix the two up: check each name in the API files below.

pyimguiDear ImGui Bundle
import imguifrom imgui_bundle import imgui
imgui.WINDOW_NO_TITLE_BARimgui.WindowFlags_.no_title_bar
imgui.create_context(), a renderer from imgui.integrations, and a loop written by handimmapp.run(gui) or hello_imgui.run(gui): the runner creates the window and runs the loop

Where to find more

The book, in plain text (if needed): https://imgui-bundle.pages.dev/llms-full.txt holds the book’s pages for users, one after the other (about 18,000 words): how to install, the core libraries, the add-ons, the notes for Python and for C++. Read it for a question that this page does not answer.

The API, in plain text (if needed): https://imgui-bundle.pages.dev/llms/api/index.txt lists one file per module (a large module, such as imgui or implot, is cut into a few files), for example https://imgui-bundle.pages.dev/llms/api/hello_imgui.txt (Hello ImGui).

The demos, in plain text (if needed): https://imgui-bundle.pages.dev/llms/demos.txt lists the demos of the catalog by category: what each one shows, the libraries it uses, and the links to its Python and C++ code. Read it to find a demo close to what the user wants, then read that demo’s code.

The explorers, where the demos run live (if needed): Two web apps show the demos in the browser, each beside its code:

Their pages draw in a canvas, with no text to read. https://imgui-bundle.pages.dev/llms/explorers.txt says what they show, and how to link to a demo section (e.g. https://pthom.github.io/imgui_explorer/?lib=implot&demo=Plots%2FLine Plots). Give such a link to a user who wants to see a feature in action.

Hello ImGui’s docs (if needed): the app runner of the bundle, and its parameters.

Fiatlight (only for work on Fiatlight itself): a library built on Dear ImGui Bundle, by the same author. https://pthom.github.io/fiatlight/flgt.pdf

Python and C++: the differences

The Python bindings follow Python’s conventions, and keep the C++ API’s structure.

1. Naming

C++ uses CamelCase, Python uses snake_case:

// C++
ImGui::BeginMainMenuBar();
ImGui::SliderFloat("My Slider", &value, 0.0f, 1.0f);
# Python
imgui.begin_main_menu_bar()
changed, value = imgui.slider_float("My Slider", value, 0.0, 1.0)

2. Return values instead of output parameters

C++ modifies a variable through a pointer. Python returns the new value:

// C++: modified through a pointer, returns whether the value changed
bool changed = ImGui::SliderFloat("value", &f, 0.0f, 1.0f);
# Python: returns whether the value changed, and the new value
changed, f = imgui.slider_float("value", f, 0.0, 1.0)

3. Enums

C++ enums become Python enum classes:

// C++ (buffer is a char[256])
bool changed = ImGui::InputText("Input", buffer, 256, ImGuiInputTextFlags_CharsUppercase);
# Python (text is a str)
changed, text = imgui.input_text("Input", text, imgui.InputTextFlags_.chars_uppercase)

4. Imports

Python imports the modules from the imgui_bundle package:

from imgui_bundle import imgui, implot, immapp, hello_imgui

# Or specific components:
from imgui_bundle import icons_fontawesome_6

C++ includes their headers:

#include "imgui.h"
#include "implot.h"
#include "hello_imgui/hello_imgui.h"
#include "immapp/immapp.h"

Common patterns and gotchas

Widget IDs

Dear ImGui identifies a widget by its label. Two widgets with the same label in the same scope conflict.

Solution 1: ## adds a hidden ID suffix:

imgui.button("OK")           # ID is "OK"
imgui.button("OK##dialog2")  # ID is "OK##dialog2", but displays as "OK"
imgui.button("##hidden")     # No visible label, ID is "##hidden"

Solution 2: push_id() / pop_id() in loops:

for i, item in enumerate(items):
    imgui.push_id(i)          # or push_id(str(i)) or push_id(item.name)
    if imgui.button("Delete"):
        delete_item(item)
    imgui.pop_id()

Begin/End pairs

Many functions come in begin/end pairs. Two rules:

  1. imgui.begin() is special: always call imgui.end(), even if begin() returns False. In Python, begin() returns two values, (expanded, opened): test the first one (a tuple is always true):

    # CORRECT
    expanded, _ = imgui.begin("Window")
    if expanded:
        imgui.text("Content")
    imgui.end()  # Always called!
    
    # WRONG - will cause errors
    expanded, _ = imgui.begin("Window")
    if expanded:
        imgui.text("Content")
        imgui.end()  # Only called when window is visible - BUG!

    For a window without a close button, imgui.begin_simple("Window") returns expanded alone, a bool: if imgui.begin_simple("Window"):, then imgui.end() after the if.

  2. Other begin/end pairs: call end_*() only if begin_*() returned True:

    if imgui.begin_menu("File"):
        if imgui.menu_item_simple("Open"):  # menu_item() takes a shortcut and a "selected" flag, and returns two values
            open_file()
        imgui.end_menu()  # Only when begin_menu returned True
    
    if imgui.begin_popup("popup"):
        imgui.text("Popup content")
        imgui.end_popup()  # Only when begin_popup returned True

Context managers (Python)

imgui_ctx calls the end_*() functions for you:

from imgui_bundle import imgui, imgui_ctx

# Automatic imgui.end() when exiting the with block
with imgui_ctx.begin("My Window") as window_visible:
    if window_visible:
        imgui.text("Hello")

# Works for other pairs too
with imgui_ctx.begin_menu("File") as menu_open:
    if menu_open:
        if imgui.menu_item_simple("Open"):
            open_file()

# Useful for tree nodes, popups, etc.
with imgui_ctx.tree_node("Settings") as node_open:
    if node_open:
        imgui.text("Settings content")

All of them: bindings/imgui_bundle/imgui_ctx.py

Sizes in em units (DPI-aware)

Never use sizes in pixels: they look wrong on high-DPI screens. Use em units: 1 em is the font’s height.

from imgui_bundle import imgui, em_to_vec2, em_size

# BAD - hardcoded pixels
imgui.button("Click", imgui.ImVec2(100, 30))

# GOOD - use em units (1 em = font height, typically ~16px at 100% DPI)
imgui.button("Click", em_to_vec2(8, 2))  # or hello_imgui.em_to_vec2(8, 2)

# Also available:
width = em_size(10)  # Single dimension
em = em_size()       # Get 1 em in pixels

em_to_vec2() and em_size() are in imgui_bundle (recommended), and also in hello_imgui and immapp.

Horizontal and vertical layouts (StackLayout)

The bundle’s Dear ImGui has StackLayout (by thedmd): begin_horizontal() / end_horizontal(), begin_vertical() / end_vertical(), and spring(). Prefer it to many same_line() calls, and for right-aligned or centered content:

imgui.begin_horizontal("toolbar", imgui.ImVec2(imgui.get_content_region_avail().x, 0.0))
imgui.button("Left")
imgui.button("Also Left")
imgui.spring()  # pushes the next items to the right
imgui.text("Right-aligned")
imgui.end_horizontal()

The same in C++, and when to use it: https://imgui-bundle.pages.dev/doc/core-libs/imgui/#horizontal-and-vertical-layouts-stacklayout

Layout helpers and small widgets (ImGuiKit)

ImGuiKit (imgui_bundle.imgui_kit) adds small helpers to Dear ImGui. Prefer them to hand-computed widths and offsets:

from imgui_bundle import imgui, imgui_kit

with imgui_kit.fill_row("volume") as row:
    _, volume = imgui.slider_float("##volume", volume, 0.0, 1.0)  # fills the line
    row.tail()  # the items after this call stay on the right
    imgui_kit.help_marker("Drag to change the volume.")

with imgui_kit.label_column("audio"):
    imgui_kit.label("Gain")
    _, gain = imgui.slider_float("##gain", gain, 0.0, 2.0)
ImGuiKit::BeginFillRow("volume");
ImGui::SliderFloat("##volume", &volume, 0.0f, 1.0f);  // fills the line
ImGuiKit::FillRowTail();                              // the items after this call stay on the right
ImGuiKit::HelpMarker("Drag to change the volume.");
ImGuiKit::EndFillRow();

Each begin_x() / end_x() pair is also a context manager named x in Python. The page: https://imgui-bundle.pages.dev/doc/addons/imgui-kit/, the API: https://imgui-bundle.pages.dev/llms/api/imgui_kit.txt

A widget that reads the mouse wheel

Hello ImGui keeps the wheel on the window it scrolls: a page scrolled with the wheel keeps scrolling over a plot. A widget that reads io.mouse_wheel itself (a value changed by the wheel, a zoom) must claim the wheel while hovered. Otherwise the page takes it, and the widget never sees it.

imgui.invisible_button("zoom", em_to_vec2(10, 10))
if imgui.set_item_key_owner(imgui.Key.mouse_wheel_y):  # hovered: the wheel is this widget's
    zoom *= 1.1 ** imgui.get_io().mouse_wheel
ImGui::InvisibleButton("zoom", HelloImGui::EmToVec2(10, 10));
if (ImGui::SetItemKeyOwner(ImGuiKey_MouseWheelY))  // hovered: the wheel is this widget's
    zoom *= powf(1.1f, ImGui::GetIO().MouseWheel);

Touch screens (phones, tablets)

An app run by Hello ImGui or ImmApp works on a touch screen with nothing to write: a swipe scrolls, a long press is a right click, a pinch scales the app or zooms a plot or an image, and a browser opens the phone’s keyboard for text fields. Its settings: RunnerParams.touch_config. An app with its own backend sets the touch layer up once: imgui_kit.touch.setup().

A widget of your own may need one call, right after it:

imgui.invisible_button("canvas", em_to_vec2(20, 12))
imgui_kit.touch.set_item_takes_drags()  # a finger's drag pans the canvas at once
pinch = imgui_kit.touch.item_pinch()  # two fingers: zoom by their spread (around pinch.center)
if pinch.active:
    zoom *= pinch.scale_delta
if imgui.set_item_key_owner(imgui.Key.mouse_wheel_y):  # the wheel zooms it too: 10% per notch
    zoom *= 1.1 ** imgui.get_io().mouse_wheel

On a desktop, imgui_kit.touch.get_config().simulate_touch = True lets the mouse play a finger, to try it.

Animations and live data: idling

Hello ImGui lowers the frame rate 3 seconds after the last user input, to save the CPU. An animation, a live image or a plot of varying data then becomes choppy. Keep it smooth:

if implot.begin_plot("Live data"):
    implot.plot_line("signal", xs, ys)
    implot.end_plot()
    hello_imgui.set_item_is_live()  # no idling while the plot is visible
if (ImPlot::BeginPlot("Live data"))
{
    ImPlot::PlotLine("signal", xs.data(), ys.data(), (int)xs.size());
    ImPlot::EndPlot();
    HelloImGui::SetItemIsLive();  // no idling while the plot is visible
}

Do not turn idling off (fps_idling.enable_idling = False, or fps_idle=0) to animate one widget. These settings are global, and the app would never idle. Keep them for an app whose whole window always moves.

Let the AI see and drive the app

You can check your own work. After a change that the user sees, run the app for a few frames and look at a screenshot. Or drive the GUI (click, type, open a header) with the ImGui Test Engine, with screenshots along the way. Do it before saying that the change works.

A screenshot of the final frame (no test engine needed):

from imgui_bundle import imgui
from imgui_bundle.immapp import testing

def gui():
    imgui.text("Hello")

testing.capture_final_frame(gui, "/tmp/out.png", window_size=(600, 400))
# then look at /tmp/out.png

Driving the GUI, with screenshots at chosen moments:

This uses the ImGui Test Engine, which has its own license (Dear ImGui Test Engine License v1.04): free for individuals, education, open-source and not-for-profit projects, and companies with a turnover under 2 million USD. Other companies need a paid license from Omar Cornut’s company: https://www.dearimgui.com/licenses. The screenshot of the final frame, above, does not use the test engine.

def gui():
    imgui.button("Click me")

def my_test(ctx: imgui.test_engine.TestContext):
    testing.capture(ctx, "/tmp/00_initial.png")
    ctx.item_click("//**/Click me")  # a widget, found by its label
    testing.capture(ctx, "/tmp/01_after_click.png")

testing.run(gui, my_test, window_size=(600, 400))

In C++: ImmApp::Testing::CaptureFinalFrame(guiFn, path, opts) and ImmApp::Testing::Capture(ctx, path), in immapp/testing.h.

Good to know:

Writing an app

The runners: Hello ImGui and ImmApp

The runners create the window, run the loop, and handle the DPI, the assets and the themes. The app gives a function that draws the GUI, called at each frame:

from imgui_bundle import imgui, immapp

def gui():
    imgui.text("Hello, world!")

immapp.run(
    gui_function=gui,           # Function called each frame
    window_title="Hello!",      # Window title
    window_size_auto=True,      # Auto-size window based on content
    with_implot=True,           # Enable ImPlot addon (optional)
    with_markdown=False,        # Enable Markdown addon (optional)
)

More settings: RunnerParams

hello_imgui.RunnerParams controls the whole app: its window, its menus, its status bar, its docking layout, its callbacks.

from imgui_bundle import hello_imgui, immapp

# Create and configure runner parameters
params = hello_imgui.RunnerParams()

# 1. Window settings
params.app_window_params.window_title = "Advanced Application"
params.app_window_params.window_geometry.size = (1200, 800)
params.app_window_params.restore_previous_geometry = True

# 2. ImGui window settings
params.imgui_window_params.show_menu_bar = True
params.imgui_window_params.show_status_bar = True
params.imgui_window_params.default_imgui_window_type = hello_imgui.DefaultImGuiWindowType.provide_full_screen_dock_space

# 3. Callbacks
params.callbacks.show_gui = my_gui_function
params.callbacks.show_menus = my_menus_function
params.callbacks.show_status = my_status_function

# 4. Run the application with full parameters
immapp.run(params)

Its parts:

All the parameters: https://pthom.github.io/hello_imgui/book/doc-params/ and https://imgui-bundle.pages.dev/llms/api/hello_imgui.txt

Async, notebooks and the browser (Pyodide)

Platformrun()run_async()
DesktopBlockingAwaitable
PyodideFire-and-forgetAwaitable
NotebookUse nb.start()Use nb.start()

Desktop, async:

import asyncio
from imgui_bundle import immapp

async def main():
    await immapp.run_async(gui, window_title="My App")
    print("GUI closed")

asyncio.run(main())

Jupyter notebooks: the nb module runs the GUI without blocking the next cells:

from imgui_bundle import immapp

# Start GUI (non-blocking, continues to next cell)
immapp.nb.start(gui, window_title="My App")

# Later, to stop:
immapp.nb.stop()

# Check if running:
if immapp.nb.is_running():
    print("GUI is active")

Pyodide (in the browser): run() starts the GUI and returns at once, since a browser cannot block. The same code works on the desktop (blocking) and in Pyodide:

immapp.run(gui, window_title="My App")

To wait for the GUI’s exit in Pyodide, use run_async():

import asyncio
async def main():
    await immapp.run_async(gui, window_title="My App")
    print("GUI closed")
asyncio.create_task(main())

More: https://imgui-bundle.pages.dev/doc/python/python-async/ and https://imgui-bundle.pages.dev/doc/python/python-pyodide/

Assets: fonts and images

The runners load the assets from a folder named assets, next to the script or the executable:

# Set custom assets folder (call before run)
hello_imgui.set_assets_folder("my_assets")

Fonts, loaded in the load_additional_fonts callback:

from imgui_bundle import hello_imgui

def load_fonts():
    # Load a font at 18px size
    hello_imgui.load_font("fonts/Roboto-Regular.ttf", 18.0)

    # Load with options (e.g., merge icons into previous font)
    font_params = hello_imgui.FontLoadingParams()
    font_params.merge_to_last_font = True
    hello_imgui.load_font("fonts/icons.ttf", 16.0, font_params)

params = hello_imgui.RunnerParams()
params.callbacks.load_additional_fonts = load_fonts

Images:

from imgui_bundle import hello_imgui, imgui

def gui():
    # Load and display an image from assets
    hello_imgui.image_from_asset("images/logo.png", imgui.ImVec2(100, 100))

    # Or with its texture, e.g. for imgui.image() (which takes an ImTextureRef)
    texture = hello_imgui.im_texture_id_from_asset("images/logo.png")
    imgui.image(imgui.ImTextureRef(texture), imgui.ImVec2(100, 100))

The assets folder, and what it holds: https://imgui-bundle.pages.dev/doc/python/python-assets/ (C++: https://imgui-bundle.pages.dev/doc/cpp/cpp-assets/)

Example programs and demos

Read these first. The whole catalog, with a line per demo and the links to its code: https://imgui-bundle.pages.dev/llms/demos.txt

Hello World

Please do read these minimal programs, in Python and C++:

A small program with ImGui, ImPlot and ImmApp

A beating heart whose pulse is controlled by a knob: a good example of the three libraries together. Please do read it:

ImPlot and ImPlot3D demos (if needed)

The full Python demos of ImPlot and ImPlot3D:

They are close translations of the C++ demos of the ImPlot and ImPlot3D repositories:

Complex layouts with Hello ImGui (if needed)

This demo shows:

In Python: bindings/imgui_bundle/demos_python/demos_immapp/demo_docking.py In C++: bindings/imgui_bundle/demos_cpp/demos_immapp/demo_docking.cpp

A custom background (if needed)

A 3D background under the GUI, with OpenGL shaders:

Pure Python backends (if needed)

For an app that runs its own loop (without Hello ImGui or ImmApp):

ImmVision

ImmVision is an image debugger: zoom, pan, pixel inspection, colormaps.

Demos: demos_python/demos_immvision/ (display, inspector, processing, linked views).