이번 강의에서는 메뉴 표시 방법을 배우겠습니다. 윈도우 기본 메모장과 유사한 텍스트 편집 프로그램을 개발하는데 상단 메뉴바 안에 저장 하위 메뉴를 배치합니다. 저장 메뉴를 클릭하면 아래 텍스트 상자에 입력한 모든 내용을 파일로 저장합니다. 과정에서 한글 등 외국 문자가 깨지는 현상을 해결하고 폰트 및 인코딩 설정법을 다룹니다. 설정을 따로 하지 않으면 영어 외 문자가 물음표로 깨져 보입니다.
저장 버튼을 누르면 시스템 파일 저장 대화상자가 팝업으로 뜨고 사용자가 저장 경로를 선택하고 파일명을 지정한 뒤 파일 저장을 완료할 수 있습니다.
아래는 프로그램 실행 화면 스크린샷입니다:

윈도우 메모장과 동일한 기본 편집 및 저장 기능을 지원하며 컴파일한 실행 파일 크기가 매우 가볍습니다.
릴리즈 모드로 컴파일하면 실행 파일 용량이 단 645kb입니다.

이제 전체 프로그램 코드를 살펴보겠습니다:
#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; // ANSI A 버전 사용
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)
{
// 윈도우 기본 한중일 폰트 경로
const char* font_path = "C:/Windows/Fonts/msyh.ttc"; // 마이크로소프트 야헤이
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);
// 핵심: 버퍼 크기를 확장하고 전체를 0으로 채워 메모리 쓰레기 데이터 제거, ??? 깨짐 현상 해결
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();
// ? 문자 깨짐 수정 핵심: 고정 버퍼 최대 길이 전달, 메모리를 0으로 초기화해 쓰레기값 없음
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)
주요 기능을 모두 함수로 캡슐화했습니다. 위 함수들은 파일 쓰기, 파일 선택 창 호출, 폰트 및 문자 인코딩 설정 역할을 수행합니다.
이전 예제와 비교했을 때 UI 부분에 메뉴바와 여러 줄 텍스트 편집 상자가 추가되었습니다.
메뉴바 구현
if (ImGui::BeginMenuBar())
{
if (ImGui::BeginMenu("파일"))
{Code language: C++ (cpp)
메뉴 렌더링은 여기서 시작하며 if (ImGui::BeginMenu(“파일”)) 구문으로 최상위 메뉴를 생성합니다.
내부에 하위 메뉴 항목을 작성하며 아래는 공식 예시 코드입니다.
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();
// ? 문자 깨짐 수정 핵심: 고정된 최대 버퍼 길이를 전달, 메모리 전체를 0으로 초기화해 쓰레기 데이터 제거
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(“저장 성공”)로 팝업 식별자 마커를 설정합니다.
아래에서 등록한 식별자에 맞춰 모달 창을 그립니다.
if (ImGui::BeginPopupModal("저장 성공", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
ImGui::Text("텍스트 파일이 정상적으로 저장되었습니다!");
if (ImGui::Button("확인", ImVec2(120, 0)))
ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}Code language: PHP (php)

이번 강의 설명은 여기까지입니다.