На этом уроке мы изучим вывод меню. Мы разработаем программу, похожую на стандартный текстовый блокнот TXT: в верхней части окна будет панель меню с подпунктом «Сохранить». При клике по пункту сохранения весь текст, введенный в текстовое поле ниже, будет записан в файл. Также мы разберем проблему искажения символов и настроим шрифт для разных языков – без правильной конфигурации неанглийские символы будут отображаться как знаки вопроса.
При попытке сохранить файл откроется системное диалоговое окно сохранения. Пользователь сможет выбрать папку для записи, задать имя файла и завершить сохранение документа.
Ниже представлен скриншот готовой запущенной программы:

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

Рассмотрим полный исходный код программы:
#pragma comment(linker, "/SUBSYSTEM:windows /ENTRY:mainCRTStartup")
#pragma execution_character_set("utf-8")
#include <GLFW/glfw3.h>
#include "imgui.h"
#include "backends/imgui_impl_glfw.h"
#include "backends/imgui_impl_opengl3.h"
#include <cstdio>
#include <string>
#include <fstream>
#include <windows.h>
#include <io.h>
#include <commdlg.h> // Обязательный заголовок для диалоговых окон работы с файлами
// Максимальный размер буфера для текста
const int TEXT_BUFFER_MAX = 4096;
std::string editor_text;
// Функция сохранения текста в файл с маркером UTF8 BOM
// Записывает содержимое текста в файл по указанному пути
bool SaveTextToFile(const char* filepath, const std::string& text)
{
std::ofstream out_file(filepath, std::ios::out | std::ios::binary);
if (!out_file.is_open())
return false;
const unsigned char utf8_bom[] = { 0xEF, 0xBB, 0xBF };
out_file.write((char*)utf8_bom, sizeof(utf8_bom));
out_file << text.c_str(); // Записывает только действительный текст до нулевого символа
out_file.close();
return true;
}
// Отображает системное диалоговое окно сохранения файла, возвращает выбранный пользователем путь
bool ShowSaveFileDialog(char* out_path, DWORD out_size)
{
OPENFILENAMEA ofn; // Используем версию A для ANSI-кодировки
ZeroMemory(&ofn, sizeof(ofn));
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = nullptr;
ofn.lpstrFilter = "Текстовые файлы\0*.txt\0Все файлы\0*.*\0"; // Фильтрация TXT или любых форматов
ofn.lpstrFile = out_path;
ofn.nMaxFile = out_size;
ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_OVERWRITEPROMPT;
ofn.lpstrDefExt = "txt";
return GetSaveFileNameA(&ofn); // Вызов функции диалога для ANSI
}
// Настройка шрифта для исправления искажения символов (знаков вопроса ???)
// Укажите путь к шрифту под вашу операционную систему или используйте системный шрифт по умолчанию
void SetupFont(ImGuiIO& io)
{
// Путь к распространенному китайскому шрифту в Windows
const char* font_path = "C:/Windows/Fonts/msyh.ttc"; // Microsoft YaHei
float font_size = 18.0f;
// Загрузка диапазона часто используемых упрощенных китайских символов
ImVector<ImWchar> ranges;
ImFontGlyphRangesBuilder builder;
builder.AddRanges(io.Fonts->GetGlyphRangesChineseSimplifiedCommon());
builder.BuildRanges(&ranges);
// Подключение пользовательского шрифта
io.Fonts->AddFontFromFileTTF(font_path, font_size, nullptr, ranges.Data);
// При необходимости многоязычной поддержки можно добавить дополнительные блоки:
// io.Fonts->AddFontFromFileTTF(font_path, font_size, nullptr, io.Fonts->GetGlyphRangesJapanese());
// io.Fonts->AddFontFromFileTTF(font_path, font_size, nullptr, io.Fonts->GetGlyphRangesKorean());
}
int main()
{
SetConsoleOutputCP(65001);
// Ключевой момент: расширяем буфер и заполняем нулями для очистки мусорных данных памяти, устраняет искажение ????
editor_text.resize(TEXT_BUFFER_MAX, '\0');
if (!glfwInit())
{
printf("Ошибка инициализации GLFW\n");
return -1;
}
const char* glsl_version = "#version 330";
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(800, 600, "Текстовый редактор заметок", nullptr, nullptr);
if (!window)
{
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGuiIO& io = ImGui::GetIO();
ImGui::StyleColorsLight();
SetupFont(io); // Загрузка шрифта для исправления искажения символов
ImGui_ImplGlfw_InitForOpenGL(window, true);
ImGui_ImplOpenGL3_Init(glsl_version);
bool show_save_success = false;
bool show_save_fail = false;
char path[MAX_PATH] = "";// Буфер для хранения пути сохраняемого файла
while (!glfwWindowShouldClose(window))
{
glfwPollEvents();
// Горячая клавиша Ctrl+S для быстрого сохранения
if (io.KeyCtrl && ImGui::IsKeyPressed(ImGuiKey_S))
{
if (ShowSaveFileDialog(path, MAX_PATH))
{
if (SaveTextToFile(path, editor_text))
show_save_success = true;
else
show_save_fail = true;
}
}
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(io.DisplaySize);
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_MenuBar; // Обязательный флаг для отображения панели меню
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::Begin("Текстовый редактор заметок", nullptr, window_flags);
// Отрисовка верхней панели меню
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("Файл"))
{
if (ImGui::MenuItem("Сохранить", "Ctrl+S")) // Пункт меню Сохранить: надпись + подсказка горячей клавиши
{
// Логика совпадает с кнопками, изученными ранее; код выполняется при клике по пункту меню
if (ShowSaveFileDialog(path, MAX_PATH)) // Открываем диалог сохранения файла
{
if (SaveTextToFile(path, editor_text)) // Выполняем запись файла
show_save_success = true; // Устанавливаем флаг успешного сохранения для последующего уведомления
else
show_save_fail = true;
}
}
ImGui::Separator();
if (ImGui::MenuItem("Выход", "Esc")) // Пункт меню закрытия программы
{
glfwSetWindowShouldClose(window, true); // Устанавливаем флаг закрытия окна, завершаем работу программы
}
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
ImGui::Spacing();
ImVec2 multiline_size = ImGui::GetContentRegionAvail();
// Основное исправление искажения ?: передаем фиксированный максимальный размер буфера, память заполнена нулями без мусора
ImGui::InputTextMultiline("##text_editor", &editor_text[0], TEXT_BUFFER_MAX, multiline_size);
// Активация модальных окон уведомлений по флагам результата сохранения
if (show_save_success)
{
// Данная строка не рисует окна и не вызывает BeginPopupModal
ImGui::OpenPopup("Сохранение выполнено");// Устанавливаем внутренний флаг ImGui для отображения модального окна, строка – уникальный ID окна
show_save_success = false;
}
if (show_save_fail)
{
ImGui::OpenPopup("Ошибка сохранения");// Флаг для окна уведомления об ошибке
show_save_fail = false;
}
// Отрисовка модального окна успешного сохранения
if (ImGui::BeginPopupModal("Сохранение выполнено", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("Текст успешно сохранен в файл!");
if (ImGui::Button("ОК", ImVec2(120, 0)))
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
// Отрисовка модального окна ошибки сохранения
if (ImGui::BeginPopupModal("Ошибка сохранения", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("Неверный путь или отсутствуют права на запись!");
if (ImGui::Button("ОК", ImVec2(120, 0)))
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
ImGui::End();
ImGui::PopStyleVar(2);
ImGui::Render();
int width, height;
glfwGetFramebufferSize(window, &width, &height);
glViewport(0, 0, width, height);
glClearColor(0.12f, 0.12f, 0.12f, 1.f);
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;
}Code language: C++ (cpp)
Я инкапсулировал всю основную логику в отдельные функции выше: запись в файл, открытие диалога выбора файла и настройка шрифтов для разных языков.
По сравнению с предыдущими примерами интерфейс дополнен двумя новыми компонентами: панелью меню и многострочным полем ввода текста.
Панель меню
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("Файл"))
{Code language: C++ (cpp)
Отрисовка меню начинается здесь. Внешний оператор if создает корневое меню через функцию ImGui::BeginMenu(«Файл»).
Внутри размещаются кликабельные подпункты. Ниже представлен официальный пример из документации ImGui:
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("Файл"))
{
if (ImGui::MenuItem("Открыть..", "Ctrl+O")) { /* Выполнение логики */ }
if (ImGui::MenuItem("Сохранить", "Ctrl+S")) { /* Выполнение логики */ }
if (ImGui::MenuItem("Закрыть", "Ctrl+W")) { my_tool_active = false; }
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}
Для создания нескольких параллельных меню:
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("Файл"))
{
if (ImGui::MenuItem("Сохранить", "Ctrl+S")) // Пункт Сохранить: надпись + подсказка горячей клавиши
{
// Работает аналогично обычным кнопкам, код внутри выполняется при клике по меню
if (ShowSaveFileDialog(path, MAX_PATH)) // Открываем системный диалог сохранения
{
if (SaveTextToFile(path, editor_text)) // Запускаем процедуру сохранения файла
show_save_success = true; // Устанавливаем флаг успешного сохранения для уведомления
else
show_save_fail = true;
}
}
ImGui::Separator();
if (ImGui::MenuItem("Выход", "Esc")) // Пункт выхода из программы
{
glfwSetWindowShouldClose(window, true); // Отправляем сигнал на закрытие окна
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Редактирование"))
{
if (ImGui::MenuItem("Копировать", "Ctrl + c"))
{
}
ImGui::EndMenu();
}
ImGui::EndMenuBar();
}Code language: C++ (cpp)
Достаточно последовательно написать несколько блоков if (ImGui::BeginMenu(«Название меню»)), чтобы разместить несколько меню рядом по горизонтали.

Важное примечание
ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoSavedSettings
| ImGuiWindowFlags_MenuBar; // Обязательный флаг, без него панель меню не будет отображатьсяCode language: JavaScript (javascript)
Флаг ImGuiWindowFlags_MenuBar обязательно нужно добавить в набор флагов окна, иначе вся панель меню не отрисуется на экране.
Многострочное поле ввода текста
Реализация многострочного редактора очень проста:
ImVec2 multiline_size = ImGui::GetContentRegionAvail();
// Основное исправление знаков ?: передаем фиксированный максимальный размер буфера, память предварительно заполнена нулями без мусорных данных
ImGui::InputTextMultiline("##text_editor", &editor_text[0], TEXT_BUFFER_MAX, multiline_size);Code language: C++ (cpp)
Достаточно вызвать функцию InputTextMultiline для создания редактора. Переменная multiline_size автоматически берет размер доступной области окна.
Модальные окна уведомлений
Принцип работы всплывающих окон: сначала мы устанавливаем внутренний флаг в ImGui, а потом отрисовываем окно по этому флагу. Пример ниже:
if (show_save_success)
{
ImGui::OpenPopup("Сохранение выполнено");Code language: CSS (css)
Когда переменная show_save_success равна true, вызываем ImGui::OpenPopup(«Сохранение выполнено») для регистрации уникального идентификатора окна уведомления.
Ниже мы отрисовываем модальное окно по ранее зарегистрированному ID:
if (ImGui::BeginPopupModal("Сохранение выполнено", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("Текст успешно сохранен в файл!");
if (ImGui::Button("ОК", ImVec2(120, 0)))
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}Code language: PHP (php)

На этом наш урок завершен.