Разработка графических интерфейсов на C++ с использованием Dear ImGui

Курс посвящен освоению парадигмы Immediate Mode GUI для создания инструментов и пользовательских интерфейсов. Вы научитесь интегрировать библиотеку в C++ проекты, работать с базовыми и интерактивными виджетами, а также управлять жизненным циклом кадра.

1. Основы Dear ImGui: архитектура Immediate Mode и настройка рабочего окружения

Основы Dear ImGui: архитектура Immediate Mode и настройка рабочего окружения

Представьте, что вам нужно создать кнопку в обычном графическом фреймворке. Вы создаете объект класса Button, регистрируете для него функцию-обработчик (callback), добавляете его в иерархию элементов окна и следите за тем, чтобы при изменении состояния программы текст на этой кнопке обновился. Если кнопка должна исчезнуть, вы должны явно удалить объект или скрыть его. В Dear ImGui всё это заменяется одной строчкой кода: if (ImGui::Button("Нажми меня")) { / действие / }. Этот парадоксальный лаконизм — не магия, а следствие фундаментального сдвига в архитектуре интерфейсов, называемого Immediate Mode GUI (IMGUI).

Философия немедленного режима против сохраненного

Большинство графических библиотек, с которыми вы могли сталкиваться (Qt, WinForms, WPF), относятся к категории Retained Mode GUI (RM_GUI). В них библиотека хранит «слепок» интерфейса в памяти. Вы строите дерево объектов, и библиотека сама решает, когда и как их перерисовывать. Это удобно для бизнес-приложений, но становится тяжеловесным в динамических средах, таких как игровые движки или инструменты визуализации данных, где состояние мира меняется 60 раз в секунду.

Dear ImGui работает иначе. Она не хранит состояние виджетов между кадрами. Каждый раз, когда ваш цикл отрисовки запускает новый кадр, вы заново описываете весь интерфейс с нуля. Если в текущем кадре вы вызвали функцию ImGui::Button(), кнопка появится на экране. Если в следующем кадре вы её не вызвали — её не существует.

Такой подход решает проблему синхронизации данных. В традиционных фреймворках часто возникает ситуация «двойного владения»: значение переменной хранится в логике программы и дублируется в текстовом поле виджета. Если логика изменила значение, нужно не забыть обновить виджет. В Dear ImGui виджет — это просто функция, которая визуализирует текущее состояние данных «здесь и сейчас».

Математика кадра и конвейер обработки

Несмотря на название "Immediate", библиотека не рисует пиксели на экране мгновенно в момент вызова функции. Вместо этого она генерирует списки команд отрисовки (draw calls). Чтобы понять, как это работает, рассмотрим упрощенную модель времени жизни одного кадра.

Пусть — общее время обработки одного кадра. Оно складывается из времени обработки логики , времени формирования интерфейса и времени работы графического API (например, OpenGL) :

Здесь — это время, затраченное процессором на выполнение ваших вызовов ImGui::Button, ImGui::Slider и т.д. Внутри этих функций Dear ImGui выполняет три задачи:

  • Layout: Рассчитывает координаты виджета на основе текущей позиции «курсора» отрисовки.
  • Input: Проверяет, находится ли курсор мыши над этими координатами и была ли нажата кнопка.
  • Geometry: Генерирует вершины (вертексы) и индексы для отрисовки прямоугольников и текста.
  • Результатом работы ImGui в конце кадра является буфер данных, содержащий массивы вершин. Каждая вершина обычно описывается структурой:

  • Позиция ()
  • Текстурные координаты ()
  • Цвет (RGBA)
  • Эти данные передаются видеокарте. Поскольку ImGui минимизирует количество переключений состояний GPU, отрисовка даже сложного интерфейса происходит чрезвычайно быстро, так как все элементы объединяются в минимальное количество пакетов (batches).

    Интеграция: Backend и Renderer

    Dear ImGui спроектирована как «безбилетник». У неё нет собственного окна, она не умеет самостоятельно открывать контекст OpenGL или DirectX и не знает, как обрабатывать системные события мыши от Windows или Linux. Она полагается на то, что у вас уже есть работающее графическое приложение.

    Процесс интеграции делится на две части:

  • Platform Backend: Отвечает за передачу событий ввода (мышь, клавиатура, геймпад) и управление системным курсором. Популярные решения: GLFW, SDL2, Win32.
  • Renderer Backend: Отвечает за превращение списков команд ImGui в конкретные вызовы графического API. Популярные решения: OpenGL 3+, DirectX 11/12, Vulkan.
  • Для новичка настройка может показаться запутанной из-за обилия файлов. В репозитории ImGui в папке backends лежат готовые реализации. Если вы используете связку GLFW + OpenGL3, вам понадобятся файлы imgui_impl_glfw.cpp и imgui_impl_opengl3.cpp.

    Настройка рабочего окружения (Step-by-Step)

    Рассмотрим процесс подключения библиотеки к проекту на C++. Мы не будем использовать менеджеры пакетов, чтобы понять структуру «руками».

    Шаг 1: Сборка исходников

    Dear ImGui — это не библиотека, которую нужно предварительно компилировать в .lib или .dll. Это набор исходных файлов, которые вы просто добавляете в свой проект. Вам необходимы:
  • imgui.cpp, imgui_draw.cpp, imgui_widgets.cpp, imgui_tables.cpp, imgui_demo.cpp
  • Заголовочные файлы: imgui.h, imconfig.h, imgui_internal.h, imstb_rectpack.h, imstb_textedit.h, imstb_truetype.h
  • Шаг 2: Инициализация

    После того как вы создали окно (например, через GLFW) и инициализировали графический контекст, необходимо «подружить» ImGui с вашей системой.

    Здесь объект ImGuiIO является ключевым: через него вы можете включать поддержку клавиатуры (io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard) или настраивать параметры шрифтов.

    Шаг 3: Главный цикл

    Типичный цикл приложения с ImGui выглядит так:

    Важный нюанс: ImGui::NewFrame() должен вызываться строго после бэкенд-функций NewFrame, так как они подготавливают дельту времени и состояние ввода.

    Управление состоянием в мире без состояний

    Поскольку ImGui "забывает" всё в конце кадра, возникает вопрос: как реализовать элементы, требующие памяти? Например, развернут ли выпадающий список или какой текст введен в поле InputText?

    Dear ImGui использует два механизма:

  • Внешнее состояние: Для данных, которые важны вашей программе (значение громкости в слайдере), вы сами выделяете переменную. Вы передаете указатель на эту переменную в функцию виджета.
  • Внутреннее состояние (ID Stack): Для чисто визуальных вещей (прокрутка окна) ImGui хранит данные в скрытом словаре. Ключом в этом словаре является идентификатор виджета.
  • Идентификатор (ID) генерируется на основе имени виджета. Если вы создадите две кнопки с одинаковым текстом "OK" в одном окне, ImGui может их перепутать (нажатие на одну вызовет срабатывание другой), так как их хеш-коды совпадут. Для решения этой проблемы используется стек идентификаторов:

    Это гарантирует уникальность, даже если визуально текст на кнопках идентичен. Понимание работы ID Stack — критический навык при создании динамических интерфейсов, где элементы создаются в цикле.

    Граничные случаи и производительность

    Часто возникает вопрос: не слишком ли накладно пересчитывать весь интерфейс 60 раз в секунду? На практике современные процессоры справляются с тысячами виджетов ImGui за доли миллисекунды. Основная нагрузка ложится не на расчеты, а на количество команд отрисовки.

    Однако есть ситуации, когда Immediate Mode требует осторожности:

  • Тяжелые вычисления внутри UI: Никогда не делайте запросы к базе данных или сложные расчеты прямо внутри if (ImGui::Button(...)). Кнопка опрашивается каждый кадр. Логика должна быть асинхронной: интерфейс только проверяет флаг готовности данных.
  • Обновление ресурсов: Если ваш интерфейс должен отображать текстуру, загруженную из файла, загружайте её один раз при инициализации, а в ImGui передавайте только её идентификатор (ID текстуры в OpenGL/DirectX).
  • Dear ImGui — это инструмент, который максимально сокращает дистанцию между "у меня есть переменная в коде" и "у меня есть ползунок для этой переменной на экране". Это делает её идеальной для инструментов отладки, редакторов уровней и любых приложений, где функциональность важнее изысканного дизайна.