Тернистый путь к графическому интерфейсу на C++
#C++
#GUI
#ImGui
#разработка
Мы встречаем свою судьбу на пути, который избираем, чтобы уйти от нее.
(с) Жан де Лафонтен
Сложный путь к созданию графического интерфейса на C++. ImGui с нами!
Содержание
- Погружаемся
- Почему?
- Готовые решения
- Создаем проект
- Время настройки
- Функционал трейнера
- Время GUI
- Проверяем работу
- Альтернативные решения
- Вместо заключения
- Полезные материалы
Погружаемся
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-приложений дело не очень эффективное. Чтобы жизнь разработчика была более счастливой, нужно воспользоваться готовыми библиотеками / фреймворками, тем более некоторые из них кроссплатформенные.
Рассмотрим список доступных решений.
Библиотека / Фреймворк | Платформа | Комментарий |
---|---|---|
Qt | Windows, macOS, Linux, Android | Богатый набор API, визуальный дизайнер, множество готовых решений |
wxWidgets | Windows, macOS, Linux | Поддержка нативных виджетов |
Dear ImGui | Любая с рендерингом | Мгновенный UI, идеален для тулзов |
FLTK | Windows, Linux, macOS | Компактная, легковесная, C-стиль |
GTKmm | Linux/Windows | C++-обёртка над GTK |
CEF / Ultralight | Windows/Linux/macOS | GUI на базе Chromium/HTML (WebView-подход) |
Как видите, выбор есть. Qt, конечно же, самое мощное решение, но стрелять из пушки по воробьям не всегда разумно. Qt подойдет для серьезных приложений, т.к. предоставит удобные инструменты и самый богатый API для работы. Но прежде чем выберем решение для дальнейшего рассмотрения, давайте сравним код на разных библиотеках и фреймворках.
Классический WinAPI
#include <windows.h>#define ID_BUTTON 1001LRESULT 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(){// Инициализация GLFWif (!glfwInit()) return -1;GLFWwindow* window = glfwCreateWindow(800, 600, "ImGui + C++", nullptr, nullptr);glfwMakeContextCurrent(window);glfwSwapInterval(1); // VSync// Инициализация Dear ImGuiIMGUI_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 |
Мультимедиа и 3D | Qt (через Qt3D) |
Интерфейс на HTML/CSS | CEF, 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 dockingcd imgui
Параметр recursive загрузит все подмодули, использованные в проекте. А параметром b мы указали явную ветку docking, в которой доступны дополнительные функции. Хотя для нашего примера можно использовать и ветку master. В результате будет создан подкаталог imgui, перейдем в него.
В репозитории имеется подкаталог examples, в котором можно найти проекты для Visual Studio с различными типами работы фреймворка: DirectX разных версий, Vulkan, OpenGL и другое.
cd .\examplesls<#Directory: E:\Develop\CppHell\imgui\examplesMode LastWriteTime Length Name---- ------------- ------ ----d---- 04.07.2025 13:09 example_allegro5d---- 04.07.2025 13:09 example_android_opengl3d---- 04.07.2025 13:09 example_apple_metald---- 04.07.2025 13:09 example_apple_opengl2d---- 04.07.2025 13:09 example_glfw_metald---- 04.07.2025 13:09 example_glfw_opengl2d---- 04.07.2025 13:09 example_glfw_opengl3d---- 04.07.2025 13:09 example_glfw_vulkand---- 04.07.2025 13:09 example_glfw_wgpud---- 04.07.2025 13:09 example_glut_opengl2d---- 04.07.2025 13:09 example_nulld---- 04.07.2025 13:09 example_sdl2_directx11d---- 04.07.2025 13:09 example_sdl2_metald---- 04.07.2025 13:09 example_sdl2_opengl2d---- 04.07.2025 13:09 example_sdl2_opengl3d---- 04.07.2025 13:09 example_sdl2_sdlrenderer2d---- 04.07.2025 13:09 example_sdl2_vulkand---- 04.07.2025 13:09 example_sdl3_opengl3d---- 04.07.2025 13:09 example_sdl3_sdlgpu3d---- 04.07.2025 13:09 example_sdl3_sdlrenderer3d---- 04.07.2025 13:09 example_sdl3_vulkand---- 04.07.2025 13:09 example_win32_directx10d---- 04.07.2025 13:09 example_win32_directx11d---- 04.07.2025 13:09 example_win32_directx12d---- 04.07.2025 13:09 example_win32_directx9d---- 04.07.2025 13:09 example_win32_opengl3d---- 04.07.2025 13:09 example_win32_vulkand---- 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 MyAppcd 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.vcxprojmv .\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 и список незагруженных успешно проектов, кроме одного (тот, что мы и скопировали).

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

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

Мы видим какое-то фоновое окно "DirectX 12...", несколько внутренних окон, а еще есть окно с консольным выводом, которое я не захватил при создании скриншота. На первый взгляд много лишнего, но все в порядке! Это же шаблон проекта, далее мы будем все настраивать под себя и уберем лишнее. На самом деле мы скопировали много лишнего в проект, но это для простоты примера. В папках backend и misc многие файлы потом можно будет удалить, но делайте это с осторожностью.
Время настройки
Рабочий шаблон у нас есть, осталось его настроить под себя. Займемся базовой настройкой.
Скроем консольный вывод
Согласитесь, что если при запуске графического приложения будет рядом открываться консоль, то выглядит это не очень корректно. Чтобы избавиться от такого поведения нужно в свойствах проекта "Компоновщик" -> "Система" изменить "Подсистему" с "Console" на "Windows".
Но это еще не всё! При попытке сборки проекта мы увидим ошибку вида:
Так происходит, потому что для подсистемы Windows точка входа должна быть другой. Если для консольного приложения была такая функция.
// Main codeint main(int, char**){// Тут остальной код приложения}То для подсистемы "Window" точка входа уже совсем другая.
// Main codeint 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();// Тут все убираем// RenderingImGui::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);// RenderingImGui::Render();// ...Запустим приложение и полюбуемся на эту красоту!
Первоначальная настройка почти готова!
- Application.h
Кириллицы нам не хватает!
При попытке использовать кириллицу мы столкнемся с ошибкой.
#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(¤tDateTime);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 более дружелюбными инструментами решать Вам, разработчикам!
А на сегодня все! Увидимся в мире разработки позже, ведь еще столько всего интересного!