Share:

Тернистый путь к графическому интерфейсу на C++

YPermitinвCPP

2025-07-05

#C++

#GUI

#ImGui

#разработка

Мы встречаем свою судьбу на пути, который избираем, чтобы уйти от нее.
(с) Жан де Лафонтен

Сложный путь к созданию графического интерфейса на C++. ImGui с нами!

Содержание

Погружаемся

C++ - могучий, быстрый и просто прекрасный язык программирования. Применяется во многих областях: от встраиваемых систем до игр. Но стоит только захотеть создать простейшее классическое десктопное приложение, на форме которого должна быть всего лишь одна кнопка, и начинаются "танцы с бубном". Вы как будто возвращаетесь в конец 90-х годов: WinAPI, дескрипторы, callback-функции и так далее.

Встает логичный вопрос: "Почему?". Почему при всей мощи C++ такая простая, на первый взгляд, задача становится такой сложной? В поисках ответа на этот вопрос мы и продолжим наш путь в этой статье, а также рассмотрим решение.

Почему?

Вопросы "Почему?" могут привести к безумию. Мы постараемся этого избежать! Причин, почему GUI на C++ - это нестерпимая боль, несколько:

  • Нет стандартного решения в виде GUI-фреймворка или чего-то подобного.

    Стандартная библиотека C++ не содержит ни одного модуля для построения приложений с графическим интерфейсом. Даже самый последний, на момент создания статьи, стандарт C++23 обделен такими полезными частями. Все, что сложнее функционала чтения данных с консоли вида "std::cin", - это работа для сторонних библиотек.

  • WinAPI имеет низкоуровневую природу.

    В контексте операционной системы Windows, для создания графических приложений используется классический WinAPI:

    • Ручное создание окон и кнопок через CreateWindowEx.
    • Обработка сообщений через WndProc.
    • Отсутствие компоновки по сетке или стилей. Все настройки делаются в коде и в таком виде, что интуитивно с этим разобраться практически невозможно.

    Что касается систем *.nix, то там ситуация не лучше. В зависимости от средств вывода, а их много разных, нужно учитывать множество настроек, версии библиотек и многое другое. В каком-то смысле, такая низкоуровневая работа с GUI в Linux еще страшнее, чем в Windows, и обычно этим никто не занимается. Да и в Windows тоже, ведь есть упрощающие работу фреймворки и библиотеки, о которых мы еще поговорим ниже.

    Примеры из десятка строк кода для показа обычной кнопки - дело пугающее. Мне страшно, а Вам?

  • Высокий порог вхождения.

    Из предыдущих пунктов следует и высокий порог вхождения, ведь для созданий GUI требуется сразу несколько серьезных компетенций:

    • Понимание событийной модели.
    • Навыки управления ресурсами (окна, буфер, шрифты, изображения, дескрипторы и многое другое).
    • Опыт работы с системами сборки и линковки нативных библиотек.

    Все это может создать непреодолимые препятствия для создания GUI-приложений на C++.

Но ничего безнадежного нет! Если есть проблема, то есть и решение. Давайте разбираться дальше.

Готовые решения

Мы поняли, что работать с низкоуровневыми API для создания GUI-приложений дело не очень эффективное. Чтобы жизнь разработчика была более счастливой, нужно воспользоваться готовыми библиотеками / фреймворками, тем более некоторые из них кроссплатформенные.

Рассмотрим список доступных решений.

Библиотека / ФреймворкПлатформаКомментарий
QtWindows, macOS, Linux, AndroidБогатый набор API, визуальный дизайнер, множество готовых решений
wxWidgetsWindows, macOS, LinuxПоддержка нативных виджетов
Dear ImGuiЛюбая с рендерингомМгновенный UI, идеален для тулзов
FLTKWindows, Linux, macOSКомпактная, легковесная, C-стиль
GTKmmLinux/WindowsC++-обёртка над GTK
CEF / UltralightWindows/Linux/macOSGUI на базе Chromium/HTML (WebView-подход)

Как видите, выбор есть. Qt, конечно же, самое мощное решение, но стрелять из пушки по воробьям не всегда разумно. Qt подойдет для серьезных приложений, т.к. предоставит удобные инструменты и самый богатый API для работы. Но прежде чем выберем решение для дальнейшего рассмотрения, давайте сравним код на разных библиотеках и фреймворках.

  • Классический WinAPI

    #include <windows.h>
    #define ID_BUTTON 1001
    LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
    {
    switch (msg)
    {
    case WM_COMMAND:
    if (LOWORD(wParam) == ID_BUTTON)
    {
    MessageBox(hwnd, L"Кнопка нажата!", L"Сообщение", MB_OK | MB_ICONINFORMATION);
    }
    break;
    case WM_DESTROY:
    PostQuitMessage(0);
    break;
    default:
    return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
    }
    int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
    {
    const wchar_t CLASS_NAME[] = L"MyWindowClass";
    WNDCLASS wc = {};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = CLASS_NAME;
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
    RegisterClass(&wc);
    HWND hwnd = CreateWindowEx(
    0, CLASS_NAME, L"Окно с кнопкой",
    WS_OVERLAPPEDWINDOW,
    CW_USEDEFAULT, CW_USEDEFAULT, 400, 200,
    nullptr, nullptr, hInstance, nullptr
    );
    if (!hwnd) return 0;
    // Создание кнопки
    CreateWindow(
    L"BUTTON", L"Нажми меня",
    WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_DEFPUSHBUTTON,
    120, 70, 150, 30,
    hwnd, (HMENU)ID_BUTTON, hInstance, nullptr
    );
    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);
    MSG msg = {};
    while (GetMessage(&msg, nullptr, 0, 0))
    {
    TranslateMessage(&msg);
    DispatchMessage(&msg);
    }
    return 0;
    }

    Мало декларативности, много ручного труда.

  • Qt, единственный и неповторимый

    #include <QApplication>
    #include <QWidget>
    #include <QPushButton>
    #include <QMessageBox>
    int main(int argc, char *argv[])
    {
    QApplication app(argc, argv);
    QWidget window;
    window.setWindowTitle("Пример Qt");
    window.resize(300, 150);
    QPushButton *button = new QPushButton("Нажми меня", &window);
    button->setGeometry(80, 50, 140, 40);
    QObject::connect(button, &QPushButton::clicked, [&]() {
    QMessageBox::information(&window, "Сообщение", "Кнопка нажата!");
    });
    window.show();
    return app.exec();
    }

    Минимум кода — максимум результата. Поддержка стилей, сигналов и слотов.

  • Dear ImGui (инструментальный UI)

    #include "imgui.h"
    #include "imgui_impl_glfw.h"
    #include "imgui_impl_opengl3.h"
    #include <GLFW/glfw3.h>
    #include <iostream>
    int main()
    {
    // Инициализация GLFW
    if (!glfwInit()) return -1;
    GLFWwindow* window = glfwCreateWindow(800, 600, "ImGui + C++", nullptr, nullptr);
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1); // VSync
    // Инициализация Dear ImGui
    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO(); (void)io;
    // Настройка стиля
    ImGui::StyleColorsDark();
    // Инициализация платформы и рендерера
    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init("#version 130");
    // Главный цикл
    while (!glfwWindowShouldClose(window))
    {
    glfwPollEvents();
    // Начало нового кадра
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplGlfw_NewFrame();
    ImGui::NewFrame();
    // Создание окна с кнопкой
    ImGui::Begin("Пример окна");
    if (ImGui::Button("Нажми меня"))
    {
    std::cout << "Кнопка нажата!" << std::endl;
    }
    ImGui::End();
    // Рендеринг
    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);
    }
    // Очистка
    ImGui_ImplOpenGL3_Shutdown();
    ImGui_ImplGlfw_Shutdown();
    ImGui::DestroyContext();
    glfwDestroyWindow(window);
    glfwTerminate();
    return 0;
    }

    Отлично подходит для отладчиков, редакторов, панелей инструментов.

Что же выбрать? Сделаем краткую таблицу с критериями выбора.

КритерийРекомендуемое решение
Нативный вид / интеграцияQt, wxWidgets
Минимальный размерFLTK
Инструменты / редакторыDear ImGui
КроссплатформенностьQt, ImGui, wxWidgets
Мультимедиа и 3DQt (через Qt3D)
Интерфейс на HTML/CSSCEF, Ultralight

Не принимайте эти критерии как истину, т.к. некоторые вещи здесь не указаны для простоты.

Что же нам нужно? В статье мы рассмотрим создание простого приложения. Для упрощения мы будем делать трейнер под Windows, значит, кроссплатформенность нам не нужна. Трейнер будет для C&C Red Alert 2: Reborn с функциями получения бесконечных денег. В целом, ничего сложного там не будет. Акцентировать внимание именно на создании трейнера мы не будем, но вот настройку фреймворка / библиотеки для GUI мы разберем детально.

Таким образом, идеальным вариантом для небольшого инструмента будет использование Dear ImGui. Вот так будет выглядеть готовый трейнер, исходный код которого Вы можете найти на GitHub. Там же Вы сможете увидеть все описанные ниже действия по созданию GUI-приложения с нуля.

Готовый трейнер

Интерфейс простой, но для сквозного примера самое то! Мы видим форму с кнопками и их обработчиками, надпись, поле ввода, простой вывод графика с динамикой изменения баланса игрока в игре, всплывающие подсказки, а также всплывающее модальное окно.

Время идти дальше!

Создаем проект

Итак, мы выбрали Dear ImGui. Теперь мы можем начать создание собственного проекта. Конечно, можно было бы с нуля создать проект в Visual Studio, настроить все зависимости сборки, компоновку и так далее. Но зачем? Мы можем за основу взять готовые примерыв репозитории проекта.

Начнем с простого. Выберем каталог для нашего проекта, например "E:\Develop\CppHell" и в нем поработаем в командной строке. Сначала клонируем полностью репозиторий Dear ImGui.

ВНИМАНИЕ!!! Вся работа в терминале в примерах выполняется через PowerShell!

cd "E:\Develop\CppHell"
git clone --recursive https://github.com/ocornut/imgui.git -b docking
cd imgui

Параметр recursive загрузит все подмодули, использованные в проекте. А параметром b мы указали явную ветку docking, в которой доступны дополнительные функции. Хотя для нашего примера можно использовать и ветку master. В результате будет создан подкаталог imgui, перейдем в него.

В репозитории имеется подкаталог examples, в котором можно найти проекты для Visual Studio с различными типами работы фреймворка: DirectX разных версий, Vulkan, OpenGL и другое.

cd .\examples
ls
<#
Directory: E:\Develop\CppHell\imgui\examples
Mode LastWriteTime Length Name
---- ------------- ------ ----
d---- 04.07.2025 13:09 example_allegro5
d---- 04.07.2025 13:09 example_android_opengl3
d---- 04.07.2025 13:09 example_apple_metal
d---- 04.07.2025 13:09 example_apple_opengl2
d---- 04.07.2025 13:09 example_glfw_metal
d---- 04.07.2025 13:09 example_glfw_opengl2
d---- 04.07.2025 13:09 example_glfw_opengl3
d---- 04.07.2025 13:09 example_glfw_vulkan
d---- 04.07.2025 13:09 example_glfw_wgpu
d---- 04.07.2025 13:09 example_glut_opengl2
d---- 04.07.2025 13:09 example_null
d---- 04.07.2025 13:09 example_sdl2_directx11
d---- 04.07.2025 13:09 example_sdl2_metal
d---- 04.07.2025 13:09 example_sdl2_opengl2
d---- 04.07.2025 13:09 example_sdl2_opengl3
d---- 04.07.2025 13:09 example_sdl2_sdlrenderer2
d---- 04.07.2025 13:09 example_sdl2_vulkan
d---- 04.07.2025 13:09 example_sdl3_opengl3
d---- 04.07.2025 13:09 example_sdl3_sdlgpu3
d---- 04.07.2025 13:09 example_sdl3_sdlrenderer3
d---- 04.07.2025 13:09 example_sdl3_vulkan
d---- 04.07.2025 13:09 example_win32_directx10
d---- 04.07.2025 13:09 example_win32_directx11
d---- 04.07.2025 13:09 example_win32_directx12
d---- 04.07.2025 13:09 example_win32_directx9
d---- 04.07.2025 13:09 example_win32_opengl3
d---- 04.07.2025 13:09 example_win32_vulkan
d---- 04.07.2025 13:09 libs
-a--- 04.07.2025 13:09 15143 imgui_examples.sln
-a--- 04.07.2025 13:09 604 README.txt
#>

Какой пример проекта нам выбрать в качестве шаблона? Т.к. мы пишем приложение для Windows, то можно использовать самый логичный, на мой взгляд, вариант это WinAPI + DirectX 12, то есть проект example_win32_directx12. Конечно, вы можете его открыть прямо здесь же через файл решения imgui_examples.sln, но тогда при разработке в IDE мы увидим много мусора. Пойдем другим путем и скопируем нужный проект и решение в свой каталог. Также скопируем все зависимости, необходимые для сборки проекта в соответствующие каталоги.

# Переходим в наш корневой каталог
cd "E:\Develop\CppHell"
# Создаем подкаталог для нашего проекта
mkdir MyApp
cd MyApp
# Создаем подкаталог для исходных файлов проекта
mkdir src
# Копируем сюда каталог проекта "example_win32_directx12" и файл решения "imgui_examples.sln"
cp "E:\Develop\CppHell\imgui\examples\imgui_examples.sln" "E:\Develop\CppHell\MyApp\src\imgui_examples.sln"
cp -r "E:\Develop\CppHell\imgui\examples\example_win32_directx12" "E:\Develop\CppHell\MyApp\src\example_win32_directx12"
# Скопируем зависимости
cp -r "E:\Develop\CppHell\imgui\backends" "E:\Develop\CppHell\MyApp\backends"
cp -r "E:\Develop\CppHell\imgui\misc" "E:\Develop\CppHell\MyApp\misc"
cp "E:\Develop\CppHell\imgui\im*" "E:\Develop\CppHell\MyApp"

В итоге мы имеем все необходимое для сборки проекта в каталоге "E:\Develop\CppHell\MyApp", кроме настроек. Да, нужно настроить проект.

Самое важное - это установленная Visual Studio 2022 с компонентами разработки десктопных приложений на C++. Мы не будем рассматривать процесс установки, оставлю лишь эту ссылку. DirectX 12 уже предустановлен в Windows 10 и Windows 11, поэтому дополнительно в этой части ничего устанавливать не придется.

Перейдем в каталог src и переименуем файлы решения и файлы проектов.

cd src
# Переимениуем файл решения
mv .\imgui_examples.sln .\MyApp.sln
# Переименуем каталог проекта
mv .\example_win32_directx12 .\MyApp
# Переименуем файлы проекта
mv .\MyApp\example_win32_directx12.vcxproj .\MyApp\MyApp.vcxproj
mv .\MyApp\example_win32_directx12.vcxproj.filters .\MyApp\MyApp.vcxproj.filters
# В файле решения заменяем подстроку имени проекта на новое значение
(Get-Content MyApp.sln) -replace 'example_win32_directx12', 'MyApp' | Set-Content MyApp.sln

Теперь откроем файл решения E:\Develop\CppHell\MyApp\src\MyApp.sln в Visual Studio 2022. При запуске нас уведомят, что не все проекты были успешно загружены. Это нормально, ведь мы скопировали всего один проект из множества. Далее IDE предложит нам конвертировать проект под более новую версию Visual Studio, т.к. изначально примеры сделаны в Visual Studio 2019.

Конвертация проекта

Со всем соглашаемся. Теперь мы видим открытую IDE и список незагруженных успешно проектов, кроме одного (тот, что мы и скопировали).

Битые проекты

Удаляем все "битые" проекты. После этого структура решения будет выглядеть так.

Итоговая структура решения

Что ж, все готово. Давайте проверим не сделали ли мы ошибок и запустим проект. Если все сделано правильно, то сразу запустится демоприложение.

Демоприложение Dear ImGui

Мы видим какое-то фоновое окно "DirectX 12...", несколько внутренних окон, а еще есть окно с консольным выводом, которое я не захватил при создании скриншота. На первый взгляд много лишнего, но все в порядке! Это же шаблон проекта, далее мы будем все настраивать под себя и уберем лишнее. На самом деле мы скопировали много лишнего в проект, но это для простоты примера. В папках backend и misc многие файлы потом можно будет удалить, но делайте это с осторожностью.

Время настройки

Рабочий шаблон у нас есть, осталось его настроить под себя. Займемся базовой настройкой.

  • Скроем консольный вывод

    Согласитесь, что если при запуске графического приложения будет рядом открываться консоль, то выглядит это не очень корректно. Чтобы избавиться от такого поведения нужно в свойствах проекта "Компоновщик" -> "Система" изменить "Подсистему" с "Console" на "Windows".

    Изменяем подсистему проекта

    Но это еще не всё! При попытке сборки проекта мы увидим ошибку вида:

    Ошибка сборки проекта

    Так происходит, потому что для подсистемы Windows точка входа должна быть другой. Если для консольного приложения была такая функция.

    // Main code
    int main(int, char**)
    {
    // Тут остальной код приложения
    }

    То для подсистемы "Window" точка входа уже совсем другая.

    // Main code
    int WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int hShowCmd
    )
    {
    // Тут остальной код приложения
    }

    Выполним эту замену в файле main.cpp и после запуска консольного окна мы больше не увидим. Ура!

  • Убираем служебное окно DirectX 12

    Фактически избавиться от этого окна полностью нельзя. Это окно и используется для отрисовки всего интерфейса. Но то, от чего нельзя избавиться - можно скрыть!

    В файле main.cpp нужно сделать несколько изменений. Первое - заменяем использование CreateWindowW на CreateWindowEx, передав туда соответствующие параметры для скрытия служебного окна и невозможности его вывода или активации в интерфейсе.

    // Этот вызов отключаем
    // HWND hwnd = ::CreateWindowW(wc.lpszClassName, L"Dear ImGui DirectX12 Example", WS_OVERLAPPEDWINDOW, 100, 100, (int)(1280 * main_scale), (int)(800 * main_scale), nullptr, nullptr, wc.hInstance, nullptr);
    // Создаем окно по своим правилам
    HWND hwnd = ::CreateWindowEx(
    // Настройки стиля окна
    WS_EX_LAYERED // Многоуровневое окно
    | WS_EX_TOPMOST // Окно должно быть помещено над всеми самыми верхними окнами и должно оставаться над ними, даже если окно деактивировано.
    | WS_EX_NOACTIVATE, // Окно верхнего уровня, созданное с помощью этого стиля, не становится окном переднего плана, когда пользователь щелкает его.
    wc.lpszClassName,
    NULL,
    WS_POPUP,
    0,
    0,
    0,
    0,
    NULL,
    NULL,
    wc.hInstance,
    NULL
    );
    SetLayeredWindowAttributes(hwnd, RGB(0, 0, 0), 0, ULW_COLORKEY);

    Но это еще не все! Нужно откорректировать цвета, чтобы сделать окно окончательно невидимым.

    // Render Dear ImGui graphics
    // Убираем
    // const float clear_color_with_alpha[4] = { clear_color.x * clear_color.w, clear_color.y * clear_color.w, clear_color.z * clear_color.w, clear_color.w };
    // Заполняем черым цветом
    const float clear_color_with_alpha[4] = { 0.0f, 0.0f, 0.0f, 0.0f };

    После внесения изменений в файл main.cpp запустим проект и увидим, что родительского фонового окна больше нет! Теперь фоном на скриншоте служит Visual Studio :)

    Дочернее окно скрыто!

    Идем дальше!

  • Убираем демо окно

    Те окна, что по умолчанию открываются при запуске проекта, это лишь демонстрация функционала библиотеки Dear ImGui. Они очень полезны для изучения работы библиотеки, т.к. в них показана работа большой части функционала компонентов, самой разметки, обработки событий и так далее. Поэтому удалять файлы этих форм из проекта сразу не стоит, они могут пригодиться. Но вот скрыть их отображение и заменить своей отрисовкой нужно.

    Как обычно идем в файл main.cpp и комментируем все, что есть между строками.

    // ...
    ImGui::NewFrame();
    // Тут все убираем
    // Rendering
    ImGui::Render();
    // ...

    Если теперь запустить приложение, то мы вообще ничего не увидим! Добавим реализацию отрисовки своего интерфейса в простейшем виде. Добавим два файла:

    • Application.h
      #pragma once
      #include "windows.h"
      namespace MyApp
      {
      void renderUI(HWND hwnd);
      }
    • Application.cpp
      #include "Application.h"
      #include "imgui.h"
      namespace MyApp
      {
      void renderUI(HWND hwnd)
      {
      ImGui::Begin("MyApp");
      ImGui::Text("Hello World!");
      ImGui::End();
      }
      }

    Теперь модифицируем файл main.cpp, чтобы в приложении вызывался новый метод renderUI(HWND hwnd), который и будет отвечать за отрисовку интерфейса нашего приложения.

    // ...
    // Не забываем включить заголовочный файл
    #include "Application.h"
    // ...
    ImGui::NewFrame();
    // Вызываем функцию отрисовки интерфейса
    MyApp::renderUI(hwnd);
    // Rendering
    ImGui::Render();
    // ...

    Запустим приложение и полюбуемся на эту красоту!

    Отрисовка работает

    Первоначальная настройка почти готова!

  • Кириллицы нам не хватает!

    При попытке использовать кириллицу мы столкнемся с ошибкой.

    #include "Application.h"
    #include "imgui.h"
    namespace MyApp
    {
    void renderUI(HWND hwnd)
    {
    ImGui::Begin("MyApp");
    ImGui::Text("Привет Мир!");
    ImGui::End();
    }
    }

    Проект просто не соберется и выдаст ошибку вида:

    Ошибка C2001 newline в константе

    Исправить эту проблему достаточно просто. Нужно сохранить проблемный файл с подходящей кодировкой, т.к. текущая стандартная windows-1251 (скорее всего она используется при добавлении нового файла в проект) и создает такую проблему.

    В Visual Studio открываем нужный файл, а дальше Файл -> Сохранить как -> Сохранить с кодировкой (возле кнопки Сохранить нажмите стрелочку). В качестве кодировки выберете UTF-8 с подписью.

    Конвертируем кодировку

    После этого сборка выполнится успешно. Но радоваться рано, т.к. после запуска мы увидим это.

    Где же буквы?

    Все дело в том, что подключенный по умолчанию к приложению шрифт не содержит необходимые символы кириллицы. Исправим это! В файле main.cpp найдем комментарий "// Load Fonts" и ниже добавим следующую строку.

    io.Fonts->AddFontFromFileTTF("c:\Windows\Fonts\arial.ttf", 20, NULL, io.Fonts->GetGlyphRangesCyrillic());

    Запустим проект еще раз и убедимся, что с кириллицей теперь все в порядке!

    Кириллица готова

Все готово для непосредственной работы над приложением. Вы еще не устали? :)

Функционал трейнера

Надеюсь, Вы еще не забыли, что мы создаем трейнер? Мы не будем описывать здесь как все это работает в части взаимодействия с процессом игры, т.к. тема эта другая. В статье Управляем игровым миром с помощью C++ / C# мы подробно рассматривали как реализовать подобное на C++ / C#.

Поэтому кратко опишем что из себя представляет класс трейнера. Заголовочный файл представляет из себя следующее:

// Ra2yrGodImpl.h
#pragma once
#include <string>
#include "windows.h"
namespace Ra2yrGodImpl
{
class Ra2yrGod
{
std::string processName;
int processId;
HANDLE processHandle;
time_t lastProcessHandleCheck;
bool processHandleValid;
HMODULE processBaseModule;
bool playerMoneyFrozen;
public:
Ra2yrGod();
~Ra2yrGod();
bool initProcess();
bool processConnected(bool forceCheck = false);
int getProcessId();
std::string getProcessName();
int getPlayerMoney();
void setPlayerMoney(int money);
bool playerMoneyFrozenState();
void freezePlayerMoney(bool enable, int targetMoney);
private:
void resetState();
void freezePlayerMoneyTask(int targetMoney);
};
}

В этом классе имеются следующие методы:

  • initProcess - подключение к процессу игры.
  • processConnected - проверка состояния подключения к процессу игры.
  • getProcessId - получить PID процесса игры.
  • getProcessName - получить имя процесса игры.
  • getPlayerMoney - получить текущий денежный баланс игрока.
  • setPlayerMoney - установить денежный баланс игрока.
  • playerMoneyFrozenState - признак, что баланс игрока заморожен.
  • freezePlayerMoney - заморозка / разморозка денежного баланса игрока.

Полную реализацию функционала трейнера Вы можете посмотреть в репозитории Ra2yrGod.

Сейчас же достаточно добавить в свой проект файлы Ra2yrGodImpl.h и Ra2yrGodImpl.cpp, а также вспомогательные функции в файлах Ra2yrGodHelper.h и Ra2yrGodHelper.cpp.

Время GUI

Мы ранее добавляли алгоритм отрисовки GUI в файле Application.cpp. Настало время реализовать отрисовку целевого интерфейса и всей логики его работы. Внесем изменения в файл Application.cpp:

// Application.cpp
#include "Application.h"
#include "Ra2yrGodImpl.h"
#include <ctime>
#include "imgui.h"
namespace Ra2yrGod
{
// Инициализация объекта для управления процессом игры
static Ra2yrGodImpl::Ra2yrGod god;
// Флаг, что окно открыто
static bool windowsShow = true;
// Текущее значение баланса игрока для отображения на форме
static int playerMoney = -1;
// Время последней проверки баланса игрока.
// Автоматически значение обновляется раз в секунду.
static time_t lastMoneyCheck;
// Данные для отрисовки на графике. Обновляются раз в секунду и отображают только данные
// за последние 60 секунд.
static float balanceHistory[60] = {};
static int balanceHistoryItem = 0;
void renderUI(HWND hwnd)
{
// Если флаг открытого окна сброшен, то значит была нажата кнопка закрытия окна.
// В этом случае выходим из приложения
if (!windowsShow)
{
exit(0);
}
// Проверяем есть ли подключение к процессу игры
bool processConnected = god.processConnected();
// Начинаем описание открытия формы
ImGui::Begin("Ra2yr God", &windowsShow);
// Определяем заголовок главной кнопки
std::string mainCommandLabel;
if (processConnected)
{
mainCommandLabel = "Переподключиться к процессу игры";
}
else
{
mainCommandLabel = "Подключиться к процессу игры";
}
// Инициализация кнопки подключения к процессу игры и обработчика ее нажатия
if (ImGui::Button(mainCommandLabel.c_str()))
{
// Подключение к процессу
god.initProcess();
// Проверяем успешность подключения к процессу игры
processConnected = god.processConnected(true);
// Считываем текущий баланс игрока
playerMoney = god.getPlayerMoney();
time(&lastMoneyCheck);
// Показываем уведомление о найденном процессе во всплывающем модальном окне
ImGui::OpenPopup("Подключение##ProcessConnect");
}
// Модальное окно открываем по центру родительского
ImVec2 parentPos = ImGui::GetWindowPos();
ImVec2 parentSize = ImGui::GetWindowSize();
ImVec2 modalSize(300, 150);
ImVec2 modalPos = ImVec2(
parentPos.x + (parentSize.x - modalSize.x) * 0.5f,
parentPos.y + (parentSize.y - modalSize.y) * 0.5f
);
ImGui::SetNextWindowPos(modalPos, ImGuiCond_Appearing);
ImGui::SetNextWindowSize(modalSize, ImGuiCond_Appearing);
// Описание модального окна
if (ImGui::BeginPopupModal("Подключение##ProcessConnect", NULL, ImGuiWindowFlags_AlwaysAutoResize))
{
int processId = god.getProcessId();
std::string processName = god.getProcessName();
if (processId > 0)
{
ImGui::Text("Процесс игры успешно подключен!
%s (%d)", processName.c_str(), processId);
}
else
{
ImGui::Text("Не удалось найти процесс игры!");
}
ImGui::Separator();
if (ImGui::Button("OK", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); }
ImGui::SetItemDefaultFocus();
ImGui::SameLine();
ImGui::EndPopup();
}
// Если подключение к процессу установлено, то отрисовываем остальной интерфейс
if (processConnected)
{
// Надпись с информацией о процессе игры
ImGui::Text("Процесс: %s (%d)", god.getProcessName().c_str(), god.getProcessId());
// Кнопка для обновления баланса игрока на форме и подсказка к ней
if (ImGui::Button("Обновить##UpdateBalance"))
{
playerMoney = god.getPlayerMoney();
time(&lastMoneyCheck);
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Обновить данные о текущем балансе игрока.");
// Следующий элемент на той же линии
ImGui::SameLine();
// Кнопка заморозки / разморозки баланса игрока
std::string frozenBalanceLabel;
if (god.playerMoneyFrozenState())
{
frozenBalanceLabel = "Разморозить";
}
else
{
frozenBalanceLabel = "Заморозить";
}
frozenBalanceLabel += "##FreezeBalance";
if (ImGui::Button(frozenBalanceLabel.c_str()))
{
bool playerMoneyFrozen = !god.playerMoneyFrozenState();
god.freezePlayerMoney(playerMoneyFrozen, playerMoney);
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip(""Заморозить" текущий баланс игрока.");
// Поле ввода с балансом игрока
if (ImGui::InputInt("##Balance", &playerMoney, 1, 1000))
{
god.setPlayerMoney(playerMoney);
};
// Если поле с балансом игрока не активно и не редактируется,
// а также с последнего обновления баланса прошло больше секунды,
// то считываем новое значение.
if (!ImGui::IsItemActivated() && !ImGui::IsItemFocused())
{
// Раз в секунду обновляем информацию о текущем балансе,
// если элемент не активен в данный момент.
time_t currentDateTime;
time(&currentDateTime);
int lastCheckTimeLeftSec = std::difftime(currentDateTime, lastMoneyCheck);
if (lastCheckTimeLeftSec >= 1)
{
playerMoney = god.getPlayerMoney();
time(&lastMoneyCheck);
balanceHistoryItem++;
if (balanceHistoryItem > 60)
balanceHistoryItem = 1;
balanceHistory[balanceHistoryItem - 1] = playerMoney;
}
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Денежный баланс игрока.");
// Отображаем график с динамикой изменения баланса игрока
ImGui::PlotLines("##BalanceStatistic", balanceHistory, 60);
}
// Завершение отрисовки формы
ImGui::End();
}
}

В листинге кода максимально прокомментировал каждый шаг по отрисовке элементов и добавлению обработчиков. Стоит отметить очень важную деталь! Отрисовка интерфейса выполняется в цикле while в main.cpp. Реализовано это так, что обновление кадров выполняется до 60 раз в секунду. То есть до 60 FPS. Поэтому в Application.cpp есть проверки частоты обновления баланса игрока и некоторые другие уже в самом классе трейнера.

Более подробно о реализации интерфейса Вы можете посмотреть в Dear ImGui.

Проверяем работу

Мы закончили работу с приложением и можно проверять его работу. Мы прошли длинный путь:

  • Выбор фреймворка / библиотеки на C++ для создания GUI.
  • Создание каркаса проекта на основе шаблона WinAPI + DirectX 12.
  • Адаптация проекта под наши нужды.
  • Добавление функционала трейнера для игры Red Alert 2: Reborn.
  • Описание интерфейса с логикой обработчиков элементов и отрисовкой.
  • Сборка и запуск приложения, проверка работы.

На самом деле работа готового трейнера не сильно отличается от того, что мы видели в самом начале.

Готовый трейнер

При появлении интереса, Вы можете самостоятельно ознакомиться с результатами труда, что мы сегодня сделали. В экспериментальном проекте трейнера Ra2yrGod Вы сможете найти весь код из примеров, а также запустить приложение у себя на компьютере собственными руками.

Удачи с этим!

Альтернативные решения

Мы хорошо поработали сегодня, но можно ли пойти другим путем? Вот несколько альтернативных вариантов.

  • C++ backend + WPF - логика на C++, UI на WPF (.NET, C#)
  • C++ backend + Blazor WebView / MAUI UI пишется на .NET, логика — на C++
  • CEF + React — визуализация на HTML, логика может быть в C++
  • C++ + Python GUI (через PyBind) — GUI на PyQt, бизнес-логика на C++

Эти варианты проще в разработке, но ничто не обгонит C++ в производительности, в т.ч. при построении GUI. Не зря разработка игр до сих пор использует C++.

Вместо заключения

Графические приложения на C++ это возможно, и в целом даже не сложно, если знать куда смотреть и что делать. Порог вхождения высокий, но и результаты в части производительности достигаются наилучшие.

Мы рассмотрели все базовые вопросы по теме создания GUI на C++. Идти этим путем или создавать GUI более дружелюбными инструментами решать Вам, разработчикам!

А на сегодня все! Увидимся в мире разработки позже, ведь еще столько всего интересного!

Y

YPermitin

.NET, TSQL, DevOps, 1C:Enterprise

Developer, just developer.

Поделиться

Другие статьи

Тернистый путь к графическому интерфейсу на C++
Тернистый путь к графическому интерфейсу на C++
Дружба между SQL Server и ClickHouse. SQLCLR снова с нами
Дружба между SQL Server и ClickHouse. SQLCLR снова с нами
Контроль дубликатов процессов в C# (.NET)
Контроль дубликатов процессов в C# (.NET)

Все статьи от автора: YPermitin

Copyright © 2025 Убежище инженера