Afficher des menus et une boîte de dialogue d’enregistrement avec ImGui – Exemple de bloc-notes TXT

Dans cette leçon, nous allons apprendre à afficher des menus. Nous concevrons un éditeur de texte similaire au bloc-notes Windows, avec une barre de menu en haut contenant une option « Enregistrer ». En cliquant sur Enregistrer, tout le texte saisi dans la zone de saisie sera écrit dans un fichier. Nous traiterons aussi le problème des caractères illisibles et configurerons la police d’écriture : sans cette configuration, les caractères non anglais s’afficheront sous forme de points d’interrogation.

Lors de l’enregistrement, une boîte de dialogue système s’ouvre. L’utilisateur peut choisir un dossier de destination, saisir un nom de fichier puis valider pour sauvegarder le contenu texte.

Voici une capture d’écran du programme en fonctionnement :

Le logiciel reprend les fonctions basiques d’édition et de sauvegarde du bloc-notes Windows, et le binaire compilé est très léger.

Compilé en mode Release, l’exécutable ne pèse que 645 ko.

Passons maintenant à l’intégralité du code source :

#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>  // En-tête obligatoire pour les boîtes de dialogue de fichiers

// Taille maximale du tampon de texte
const int TEXT_BUFFER_MAX = 4096;
std::string editor_text;

// Enregistrer le texte dans un fichier avec un marqueur UTF-8 BOM
// Écrire le contenu texte dans le fichier cible
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(); // N’écrire que le texte valide jusqu’au caractère nul
    out_file.close();
    return true;
}

// Ouvrir la boîte de dialogue d’enregistrement système, renvoyer le chemin sélectionné
bool ShowSaveFileDialog(char* out_path, DWORD out_size)
{
    OPENFILENAMEA ofn;   // Utiliser la version ANSI A
    ZeroMemory(&ofn, sizeof(ofn));
    ofn.lStructSize = sizeof(ofn);
    ofn.hwndOwner = nullptr;
    ofn.lpstrFilter = "Fichiers texte\0*.txt\0Tous les fichiers\0*.*\0"; // Filtre .txt ou tous formats
    ofn.lpstrFile = out_path;
    ofn.nMaxFile = out_size;
    ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_OVERWRITEPROMPT;
    ofn.lpstrDefExt = "txt";

    return GetSaveFileNameA(&ofn);   // Appel de la fonction de dialogue ANSI
}

// Configurer la police pour corriger les caractères ? illisibles
// Indiquer le chemin de police adapté à votre environnement ou utiliser la police système par défaut
void SetupFont(ImGuiIO& io)
{
    // Chemin d’une police chinoise courante sous Windows
    const char* font_path = "C:/Windows/Fonts/msyh.ttc"; // Microsoft YaHei
    float font_size = 18.0f;

    // Charger la plage de caractères chinois simplifiés courants
    ImVector<ImWchar> ranges;
    ImFontGlyphRangesBuilder builder;
    builder.AddRanges(io.Fonts->GetGlyphRangesChineseSimplifiedCommon());
    builder.BuildRanges(&ranges);

    // Charger la police personnalisée
    io.Fonts->AddFontFromFileTTF(font_path, font_size, nullptr, ranges.Data);

    // Ajouter des plages de caractères pour le multilinguisme si nécessaire :
    // 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);
    // Point crucial : agrandir le tampon et le remplir de zéros pour supprimer les données résiduelles mémoire, corriger les ???
    editor_text.resize(TEXT_BUFFER_MAX, '\0');

    if (!glfwInit())
    {
        printf("Échec de l’initialisation de 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, "Éditeur de notes texte", nullptr, nullptr);
    if (!window)
    {
        glfwTerminate();
        return -1;
    }
    glfwMakeContextCurrent(window);
    glfwSwapInterval(1);

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

    SetupFont(io); // Charger la police pour résoudre les caractères corrompus

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

    bool show_save_success = false;
    bool show_save_fail = false;

	char path[MAX_PATH] = "";// Tampon de stockage du chemin de fichier

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

        // Raccourci Ctrl+S pour enregistrer
        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;   // Drapeau obligatoire pour afficher la barre de menu

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

        ImGui::Begin("Éditeur de notes texte", nullptr, window_flags);

        // Rendu de la barre de menu supérieure
        if (ImGui::BeginMenuBar())
        {
            if (ImGui::BeginMenu("Fichier"))
            {
				if (ImGui::MenuItem("Enregistrer", "Ctrl+S")) // Option de menu Enregistrer : libellé + indication raccourci
                {
					// Même logique que les boutons vus précédemment, le code s’exécute au clic du menu
					if (ShowSaveFileDialog(path, MAX_PATH)) // Ouvrir la boîte de dialogue d’enregistrement
                    {
						if (SaveTextToFile(path, editor_text)) // Exécuter l’enregistrement du fichier
                            show_save_success = true; // Activer le drapeau de succès pour afficher une fenêtre de notification
                        else
                            show_save_fail = true;
                    }
                }

                ImGui::Separator();

                if (ImGui::MenuItem("Quitter", "Échap")) // Option Quitter
                {
					glfwSetWindowShouldClose(window, true); // Activer le drapeau de fermeture pour terminer le programme
                }
                ImGui::EndMenu();
            }
            ImGui::EndMenuBar();
        }

        ImGui::Spacing();

        ImVec2 multiline_size = ImGui::GetContentRegionAvail();
        // Correction des ?: transmettre une taille maximale fixe, tampon initialisé à zéro sans données parasites
        ImGui::InputTextMultiline("##text_editor", &editor_text[0], TEXT_BUFFER_MAX, multiline_size);

	// Déclencher les popups selon le résultat de l’enregistrement
        if (show_save_success)
        {
            // Ne dessine aucune fenêtre ni n’appelle BeginPopupModal
	    ImGui::OpenPopup("Enregistrement réussi");// Enregistrer un marqueur interne ImGui pour afficher une fenêtre modale, chaîne = identifiant unique
            show_save_success = false; 
        }
        if (show_save_fail)
        {
			ImGui::OpenPopup("Échec de l’enregistrement");// Marqueur pour la fenêtre d’erreur
            show_save_fail = false;
        }

	// Afficher la fenêtre modale de succès
        if (ImGui::BeginPopupModal("Enregistrement réussi", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
        {
            ImGui::Text("Le texte a été enregistré avec succès !");
            if (ImGui::Button("OK", ImVec2(120, 0)))
                ImGui::CloseCurrentPopup();
            ImGui::EndPopup();
        }

	// Afficher la fenêtre modale d’échec
        if (ImGui::BeginPopupModal("Échec de l’enregistrement", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
        {
            ImGui::Text("Chemin invalide ou absence de permissions d’écriture !");
            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;
}Langage du code : C++ (cpp)

J’ai encapsulé toute la logique principale dans des fonctions distinctes : écriture de fichier, appel de la boîte de dialogue de sélection de fichier et configuration de la police d’affichage.

Par rapport aux exemples précédents, l’interface ajoute deux composants majeurs : une barre de menu et une zone de saisie multiligne.

Barre de menu

if (ImGui::BeginMenuBar())
{
    if (ImGui::BeginMenu("Fichier"))
    {Langage du code : C++ (cpp)

Le rendu du menu commence ici : le bloc if (ImGui::BeginMenu(« Fichier »)) crée un menu de premier niveau.

À l’intérieur se trouvent les sous-options cliquables. Voici l’exemple officiel de la bibliothèque ImGui :

if (ImGui::BeginMenuBar())
{
    if (ImGui::BeginMenu("Fichier"))
    {
        if (ImGui::MenuItem("Ouvrir..", "Ctrl+O")) { /* Traitement */ }
        if (ImGui::MenuItem("Enregistrer", "Ctrl+S"))   { /* Traitement */ }
        if (ImGui::MenuItem("Fermer", "Ctrl+W"))  { my_tool_active = false; }
        ImGui::EndMenu();
    }
    ImGui::EndMenuBar();
}

Pour ajouter plusieurs menus côte à côte :

if (ImGui::BeginMenuBar())
{
    if (ImGui::BeginMenu("Fichier"))
    {
	if (ImGui::MenuItem("Enregistrer", "Ctrl+S")) // Option Enregistrer : libellé + indication raccourci
        {
	    // Même fonctionnement qu’un bouton classique, le code s’exécute au clic
	   if (ShowSaveFileDialog(path, MAX_PATH)) // Ouvrir la boîte de dialogue d’enregistrement
            {
		if (SaveTextToFile(path, editor_text)) // Lancer l’enregistrement
                    show_save_success = true; // Activer le drapeau pour la notification
                else
                    show_save_fail = true;
            }
        }

        ImGui::Separator();

        if (ImGui::MenuItem("Quitter", "Échap")) // Option Quitter
        {
			glfwSetWindowShouldClose(window, true); // Signaler la fermeture de la fenêtre
        }
        ImGui::EndMenu();
    }

    if (ImGui::BeginMenu("Édition"))
    {
        if (ImGui::MenuItem("Copier", "Ctrl + c")) 
        {

        }
        ImGui::EndMenu();
    }

    ImGui::EndMenuBar();
}Langage du code : C++ (cpp)

Il suffit d’empiler plusieurs blocs if (ImGui::BeginMenu(« NomMenu »)) pour afficher plusieurs menus horizontalement.

Important

ImGuiWindowFlags window_flags = ImGuiWindowFlags_NoDecoration
    | ImGuiWindowFlags_NoMove
    | ImGuiWindowFlags_NoResize
    | ImGuiWindowFlags_NoSavedSettings
    | ImGuiWindowFlags_MenuBar;   // Drapeau indispensable sans lequel la barre de menu ne s’affichera pasLangage du code : JavaScript (javascript)

Il faut absolument ajouter ImGuiWindowFlags_MenuBar aux drapeaux de fenêtre, sinon la barre de menu ne sera jamais rendue.

Zone de saisie multiligne

La zone de saisie multiligne est très simple à mettre en place :

ImVec2 multiline_size = ImGui::GetContentRegionAvail();
// Correction des caractères ?: transmettre une taille de tampon maximale fixe, mémoire initialisée à zéro sans déchets
ImGui::InputTextMultiline("##text_editor", &editor_text[0], TEXT_BUFFER_MAX, multiline_size);Langage du code : C++ (cpp)

Appelez InputTextMultiline pour créer l’éditeur. multiline_size récupère automatiquement la taille de l’espace disponible.

Fenêtres de dialogue modales

Les fenêtres de notification fonctionnent en deux étapes : d’abord définir un drapeau interne à ImGui, puis dessiner la fenêtre correspondante en fonction de ce drapeau. Exemple :

if (show_save_success)
{
    ImGui::OpenPopup("Enregistrement réussi");Langage du code : CSS (css)

Lorsque show_save_success vaut true, on utilise ImGui::OpenPopup(« Enregistrement réussi ») pour enregistrer un identifiant de popup.

Plus bas, on dessine la fenêtre modale associée à cet identifiant :

if (ImGui::BeginPopupModal("Enregistrement réussi", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
    ImGui::Text("Le texte a été enregistré avec succès !");
    if (ImGui::Button("OK", ImVec2(120, 0)))
        ImGui::CloseCurrentPopup();
    ImGui::EndPopup();
}Langage du code : PHP (php)

C’est tout pour cette leçon !

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *