Exibir menus e caixa de diálogo de salvar com ImGui – Criar exemplo de bloco de notas TXT

Nesta aula, vamos aprender a renderizar menus. Vamos desenvolver um programa similar ao bloco de notas TXT, com uma barra de menu no topo contendo o subitem Salvar. Ao clicar em Salvar, todo o texto digitado na caixa abaixo será gravado em um arquivo. Vamos também resolver o problema de caracteres ilegíveis e configurar a fonte de texto; sem essa configuração, caracteres não ingleses aparecerão como pontos de interrogação.

Ao salvar, uma caixa de diálogo nativa de salvar arquivo irá abrir. O usuário pode escolher a pasta de destino, digitar um nome de arquivo e concluir o salvamento.

Abaixo está a captura de tela do programa em execução:

É semelhante ao bloco de notas do Windows, conta com funções básicas de edição e salvamento, e o executável compilado é muito leve.

Quando compilado no modo Release, o arquivo tem apenas 645kb.

Vamos analisar o código completo do 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>  // Cabeçalho obrigatório para caixas de diálogo de arquivo

// Tamanho máximo do buffer de texto
const int TEXT_BUFFER_MAX = 4096;
std::string editor_text;

// Salvar texto em arquivo com marca UTF8 BOM
// Grava o conteúdo textual no arquivo especificado
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(); // Apenas escreve texto válido até o caractere nulo
    out_file.close();
    return true;
}

// Abrir caixa de diálogo de salvar e retornar o caminho escolhido pelo usuário
bool ShowSaveFileDialog(char* out_path, DWORD out_size)
{
    OPENFILENAMEA ofn;   // Utilizar versão A (ANSI)
    ZeroMemory(&ofn, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = nullptr;
    ofn.lpstrFilter = "Arquivos de Texto\0*.txt\0Todos os Arquivos\0*.*\0"; // Filtro para txt ou todos os formatos
    ofn.lpstrFile = out_path;
    ofn.nMaxFile = out_size;
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_OVERWRITEPROMPT;
    ofn.lpstrDefExt = "txt";

    return GetSaveFileNameA(&ofn);   // Chamar função de diálogo ANSI
}

// Configurar fonte para corrigir problema de caracteres ??? ilegíveis
// Insira o caminho da fonte compatível com seu ambiente ou use a fonte padrão do sistema
void SetupFont(ImGuiIO& io)
{
    // Caminho de fonte chinês comum no Windows
    const char* font_path = "C:/Windows/Fonts/msyh.ttc"; // Microsoft YaHei
    float font_size = 18.0f;

    // Carregar conjunto de caracteres chineses simplificados comuns
    ImVector<ImWchar> ranges;
    ImFontGlyphRangesBuilder builder;
    builder.AddRanges(io.Fonts->GetGlyphRangesChineseSimplifiedCommon());
    builder.BuildRanges(&ranges);

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

    // Adicione blocos extras para suporte multilíngue se necessário:
    // 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);
    // Ponto essencial: redimensionar buffer e preencher com zeros para remover lixo de memória, corrige ????
    editor_text.resize(TEXT_BUFFER_MAX, '\0');

    if (!glfwInit())
    {
        printf("Falha na inicialização do 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); // Carregar fonte para resolver caracteres quebrados

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

    bool show_save_success = false;
    bool show_save_fail = false;

	char path[MAX_PATH] = "";// Buffer para armazenar caminho do arquivo salvo

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

        // Atalho Ctrl+S para salvar
        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;   // Flag obrigatória para renderizar barra de menu

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

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

        // Renderizar barra de menu superior
        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("Arquivo"))
            {
				if (ImGui::MenuItem("Salvar", "Ctrl+S")) // Item Salvar: rótulo + dica de atalho
                {
					// Mesma lógica dos botões que estudamos antes, código executa ao clicar no menu
					if (ShowSaveFileDialog(path, MAX_PATH)) // Abrir caixa de diálogo de salvar
                    {
						if (SaveTextToFile(path, editor_text)) // Executar gravação do arquivo
                            show_save_success = true; // Marcar sucesso para exibir popup depois
                        else
                            show_save_fail = true;
                    }
                }

                ImGui::Separator();

                if (ImGui::MenuItem("Sair", "Esc")) // Item para fechar o programa
                {
					glfwSetWindowShouldClose(window, true); // Definir flag de fechamento para encerrar app
                }
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::Spacing();

        ImVec2 multiline_size = ImGui::GetContentRegionAvail();
        // Solução principal para ?: passar tamanho máximo fixo do buffer, memória zerada sem dados residuais
        ImGui::InputTextMultiline("##text_editor", &editor_text[0], TEXT_BUFFER_MAX, multiline_size);

	// Acionar popups com base no resultado da gravação
        if (show_save_success)
        {
            // Essa linha não desenha janelas nem executa BeginPopupModal
	    ImGui::OpenPopup("Salvo com Sucesso");// Marca interna no ImGui para abrir modal, string é ID único do popup
            show_save_success = false; 
        }
        if (show_save_fail)
        {
			ImGui::OpenPopup("Falha ao Salvar");// Marca para popup de erro
            show_save_fail = false;
        }

	// Desenhar modal de sucesso
        if (ImGui::BeginPopupModal("Salvo com Sucesso", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
        {
            ImGui::Text("Texto salvo com sucesso!");
            if (ImGui::Button("OK", ImVec2(120, 0)))
                ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
        }

	// Desenhar modal de falha
        if (ImGui::BeginPopupModal("Falha ao Salvar", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
        {
            ImGui::Text("Caminho inválido ou sem permissão de escrita!");
            if (ImGui::Button("OK", 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)

Encapsulei toda a lógica principal em funções separadas: gravação de arquivo, abertura de diálogo de arquivo e configuração de fonte para idiomas.

Comparado com exemplos anteriores, a interface ganhou dois novos componentes: barra de menu e caixa de texto multilinha.

Barra de Menu

if (ImGui::BeginMenuBar())
{
    if (ImGui::BeginMenu("Arquivo"))
    {Code language: C++ (cpp)

A renderização do menu começa aqui. O if externo cria um menu principal com ImGui::BeginMenu(“Arquivo”).

Dentro ficam os itens clicáveis do submenu. Abaixo o exemplo oficial da biblioteca ImGui:

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

Para adicionar vários menus lado a lado:

if (ImGui::BeginMenuBar())
{
    if (ImGui::BeginMenu("Arquivo"))
    {
	if (ImGui::MenuItem("Salvar", "Ctrl+S")) // Item Salvar: texto + dica de atalho
        {
	    // Funciona igual botões normais, código roda ao clicar o menu
	   if (ShowSaveFileDialog(path, MAX_PATH)) // Abrir caixa de diálogo de salvar
            {
		if (SaveTextToFile(path, editor_text)) // Executar gravação
                    show_save_success = true; // Ativar flag para popup de aviso
                else
                    show_save_fail = true;
            }
        }

        ImGui::Separator();

        if (ImGui::MenuItem("Sair", "Esc")) // Item Sair
        {
			glfwSetWindowShouldClose(window, true); // Sinalizar fechamento da janela
        }
        ImGui::EndMenu();
    }

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

        }
        ImGui::EndMenu();
    }

    ImGui::EndMenuBar();
}Code language: C++ (cpp)

Basta escrever vários blocos if (ImGui::BeginMenu(“Nome”)) sequencialmente para exibir menus paralelos.

Aviso Importante

ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration
    | ImGuiWindowFlags_NoMove
    | ImGuiWindowFlags_NoResize
    | ImGuiWindowFlags_NoSavedSettings
    | ImGuiWindowFlags_MenuBar;   // Flag obrigatória, sem ela a barra de menu não apareceCode language: JavaScript (javascript)

Você precisa adicionar ImGuiWindowFlags_MenuBar nas flags da janela, caso contrário a barra de menu nunca será renderizada.

Caixa de Texto Multilinha

A implementação de texto multilinha é muito simples:

ImVec2 multiline_size = ImGui::GetContentRegionAvail();
// Solução para caracteres ?: passar tamanho máximo fixo do buffer, memória limpa de lixo
ImGui::InputTextMultiline("##text_editor", &editor_text[0], TEXT_BUFFER_MAX, multiline_size);Code language: C++ (cpp)

Chame InputTextMultiline para criar o editor. multiline_size captura automaticamente o tamanho da área disponível.

Caixas de Aviso Popup

Os popups funcionam assim: primeiro definimos uma flag interna no ImGui, depois renderizamos a janela correspondente baseada nela. Exemplo:

if (show_save_success)
{
    ImGui::OpenPopup("Salvo com Sucesso");Code language: CSS (css)

Quando show_save_success for true, usamos ImGui::OpenPopup(“Salvo com Sucesso”) para registrar o ID do popup.

Abaixo desenhamos a janela modal correspondente ao ID registrado:

if (ImGui::BeginPopupModal("Salvo com Sucesso", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
    ImGui::Text("Texto salvo com sucesso!");
    if (ImGui::Button("OK", ImVec2(120, 0)))
        ImGui::CloseCurrentPopup();
    ImGui::EndPopup();
}Code language: PHP (php)

Isso é tudo para esta aula.

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *