Mostrar menús y diálogo de guardado con ImGui – Ejemplo de bloc de notas TXT

En esta lección aprenderemos a mostrar menús. Vamos a desarrollar una aplicación similar al bloc de notas TXT, con una barra de menú superior que incluye una subopción Guardar. Al pulsar Guardar, se guardará todo el texto escrito en el recuadro inferior. Aquí trataremos el problema de caracteres corruptos y aprenderemos a configurar la fuente: si no se configura, los caracteres que no estén en inglés se verán deformados con signos de interrogación.

Al guardar, se abrirá un diálogo nativo de guardado de archivos. El usuario podrá elegir la carpeta destino, escribir un nombre de archivo y completar el guardado.

A continuación tienes la captura de pantalla del programa en ejecución:

Es similar al bloc de notas de Windows, cuenta con funciones básicas de edición y guardado, y el archivo compilado es muy ligero.

Si compilamos en modo Release, solo ocupa 645kb.

Veamos el código completo del programa:

#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>  // Cabecera necesaria para los diálogos de archivo

// Límite máximo del búfer de texto
const int TEXT_BUFFER_MAX = 4096;
std::string editor_text;

// Función para guardar texto en archivo con marca UTF8 BOM
// Escribe el contenido textual en el archivo destino
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(); // Solo escribe el texto válido hasta el terminador nulo
    out_file.close();
    return true;
}

// Muestra el diálogo nativo de guardado y devuelve la ruta seleccionada por el usuario
bool ShowSaveFileDialog(char* out_path, DWORD out_size)
{
    OPENFILENAMEA ofn;   // Usamos la versión A (ANSI)
    ZeroMemory(&ofn, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = nullptr;
    ofn.lpstrFilter = "Archivos de texto\0*.txt\0Todos los archivos\0*.*\0"; // Filtro para txt o cualquier formato
    ofn.lpstrFile = out_path;
    ofn.nMaxFile = out_size;
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_OVERWRITEPROMPT;
    ofn.lpstrDefExt = "txt";

    return GetSaveFileNameA(&ofn);   // Llamada a la función de diálogo ANSI
}

// Configurar fuente para solucionar el problema de signos de interrogación
// Escribe la ruta de la fuente correspondiente a tu entorno o usa la fuente por defecto del sistema
void SetupFont(ImGuiIO& io)
{
    // Ruta común de fuente china en Windows
    const char* font_path = "C:/Windows/Fonts/msyh.ttc"; // Microsoft YaHei
    float font_size = 18.0f;

    // Cargar rango de caracteres chinos simplificados habituales
    ImVector<ImWchar> ranges;
    ImFontGlyphRangesBuilder builder;
    builder.AddRanges(io.Fonts->GetGlyphRangesChineseSimplifiedCommon());
    builder.BuildRanges(&ranges);

    // Cargar fuente personalizada
    io.Fonts->AddFontFromFileTTF(font_path, font_size, nullptr, ranges.Data);

    // Si necesitas soporte multilingüe, puedes agregar lo siguiente:
    // 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);
    // Punto crucial: ampliar el búfer y rellenarlo con ceros para borrar datos residuales de memoria, soluciona los ????
    editor_text.resize(TEXT_BUFFER_MAX, '\0');

    if (!glfwInit())
    {
        printf("Fallo al inicializar 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, "Editor de Notas de Texto", nullptr, nullptr);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();
    ImGui::StyleColorsLight();

    SetupFont(io); // Cargar fuente para corregir caracteres corruptos

    ImGui_ImplGlfw_InitForOpenGL(window, true);
    ImGui_ImplOpenGL3_Init(glsl_version);

    bool show_save_success = false;
    bool show_save_fail = false;

	char path[MAX_PATH] = "";// Búfer para almacenar la ruta del archivo guardado

    while (!glfwWindowShouldClose(window))
    {
        glfwPollEvents();

        // Atajo de teclado Ctrl+S para guardar
        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;   // Bandera obligatoria para mostrar la barra de menú

        ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
        ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);

        ImGui::Begin("Editor de Notas de Texto", nullptr, window_flags);

        // Renderizado de la barra de menú superior
        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("Archivo"))
            {
				if (ImGui::MenuItem("Guardar", "Ctrl+S")) // Opción Guardar: texto visible + indicador de atajo
                {
					// Funciona igual que los botones vistos anteriormente, el código se ejecuta al pulsar el menú
					if (ShowSaveFileDialog(path, MAX_PATH)) // Abrir diálogo de guardado
                    {
						if (SaveTextToFile(path, editor_text)) // Ejecutar guardado del archivo
                            show_save_success = true; // Marcar éxito para mostrar ventana de aviso después
                        else
                            show_save_fail = true;
                    }
                }

                ImGui::Separator();

                if (ImGui::MenuItem("Salir", "Esc")) // Opción para cerrar el programa
                {
					glfwSetWindowShouldClose(window, true); // Activar bandera de cierre para finalizar la app
                }
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::Spacing();

        ImVec2 multiline_size = ImGui::GetContentRegionAvail();
        // Solución principal a los ?: pasar longitud máxima fija de búfer, memoria inicializada en cero sin datos residuales
        ImGui::InputTextMultiline("##text_editor", &editor_text[0], TEXT_BUFFER_MAX, multiline_size);

	// Activar popups según el resultado del guardado
        if (show_save_success)
        {
            // Esta línea no dibuja ventanas ni ejecuta BeginPopupModal
	    ImGui::OpenPopup("Guardado Exitoso");// Establece una marca interna en ImGui para mostrar un modal, el texto es ID único del popup
            show_save_success = false; 
        }
        if (show_save_fail)
        {
			ImGui::OpenPopup("Error al Guardar");// Marca para el popup de error
            show_save_fail = false;
        }

	// Dibujar modal de éxito
        if (ImGui::BeginPopupModal("Guardado Exitoso", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
        {
            ImGui::Text("El texto se guardó correctamente!");
            if (ImGui::Button("Aceptar", ImVec2(120, 0)))
                ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
        }

	// Dibujar modal de error
        if (ImGui::BeginPopupModal("Error al Guardar", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
        {
            ImGui::Text("Ruta inválida o sin permisos de escritura!");
            if (ImGui::Button("Aceptar", 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;
}Lenguaje del código: C++ (cpp)

He encapsulado toda la lógica básica en funciones independientes: escritura de archivos, apertura de diálogos de selección y configuración de fuentes para idiomas.

En comparación con ejemplos anteriores, la interfaz añade dos componentes nuevos: barra de menú y campo de texto multilínea.

Barra de menú

if (ImGui::BeginMenuBar())
{
    if (ImGui::BeginMenu("Archivo"))
    {Lenguaje del código: C++ (cpp)

Aquí empieza el renderizado de menús. El if exterior crea un menú principal con ImGui::BeginMenu(«Archivo»).

Dentro se colocan los elementos clickeables del submenú. Abajo tienes el ejemplo oficial de la librería:

if (ImGui::BeginMenuBar())
{
    if (ImGui::BeginMenu("Archivo"))
    {
        if (ImGui::MenuItem("Abrir..", "Ctrl+O")) { /* Código funcional */ }
        if (ImGui::MenuItem("Guardar", "Ctrl+S"))   { /* Código funcional */ }
        if (ImGui::MenuItem("Cerrar", "Ctrl+W"))  { my_tool_active = false; }
        ImGui::EndMenu();
    }
    ImGui::EndMenuBar();
}

Para agregar varios menús paralelos:

if (ImGui::BeginMenuBar())
{
    if (ImGui::BeginMenu("Archivo"))
    {
	if (ImGui::MenuItem("Guardar", "Ctrl+S")) // Opción Guardar: texto + indicador de atajo
        {
	    // Funciona igual que botones normales, el código se ejecuta al clickar el menú
	   if (ShowSaveFileDialog(path, MAX_PATH)) // Abrir diálogo de guardado
            {
		if (SaveTextToFile(path, editor_text)) // Ejecutar guardado
                    show_save_success = true; // Activar bandera para aviso popup
                else
                    show_save_fail = true;
            }
        }

        ImGui::Separator();

        if (ImGui::MenuItem("Salir", "Esc")) // Opción Salir
        {
			glfwSetWindowShouldClose(window, true); // Señal para cerrar ventana
        }
        ImGui::EndMenu();
    }

    if (ImGui::BeginMenu("Editar"))
    {
        if (ImGui::MenuItem("Copiar", "Ctrl + c")) 
        {

        }
        ImGui::EndMenu();
    }

    ImGui::EndMenuBar();
}Lenguaje del código: C++ (cpp)

Solo tienes que escribir varios bloques if (ImGui::BeginMenu(«Nombre»)) uno detrás de otro para mostrar menús en fila.

Nota importante

ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration
    | ImGuiWindowFlags_NoMove
    | ImGuiWindowFlags_NoResize
    | ImGuiWindowFlags_NoSavedSettings
    | ImGuiWindowFlags_MenuBar;   // Bandera imprescindible, sin ella no se verá el menúLenguaje del código: JavaScript (javascript)

Debes incluir ImGuiWindowFlags_MenuBar en las banderas de ventana, si lo omites la barra de menú no se renderizará nunca.

Campo de texto multilínea

El editor multilínea es muy sencillo de implementar:

ImVec2 multiline_size = ImGui::GetContentRegionAvail();
// Solución a signos ?: pasar tamaño máximo fijo de búfer, memoria vaciada de datos residuales
ImGui::InputTextMultiline("##text_editor", &editor_text[0], TEXT_BUFFER_MAX, multiline_size);Lenguaje del código: C++ (cpp)

Llama a InputTextMultiline para crear el editor. multiline_size se ajusta automáticamente al espacio disponible.

Ventanas emergentes de aviso

Los popups funcionan marcando primero una bandera interna en ImGui, luego renderizar la ventana según esa marca. Ejemplo:

if (show_save_success)
{
    ImGui::OpenPopup("Guardado Exitoso");Lenguaje del código: CSS (css)

Cuando show_save_success sea true, usamos ImGui::OpenPopup(«Guardado Exitoso») para registrar el identificador del popup.

Abajo dibujamos el modal correspondiente al ID registrado:

if (ImGui::BeginPopupModal("Guardado Exitoso", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
    ImGui::Text("El texto se guardó correctamente!");
    if (ImGui::Button("Aceptar", ImVec2(120, 0)))
        ImGui::CloseCurrentPopup();
    ImGui::EndPopup();
}Lenguaje del código: PHP (php)

Esto es todo para esta lección.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *