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 !