Отображение меню и диалога сохранения через ImGui – пример создания текстового блокнота TXT

На этом уроке мы изучим вывод меню. Мы разработаем программу, похожую на стандартный текстовый блокнот 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)

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *