ImGui實現選單與儲存對話視窗 – 建立TXT記事本範例

本課程將教大家繪製介面選單,我們會實作類似TXT記事本的程式,視窗頂部配置選單列,選單內提供「儲存」子選項。點擊儲存後,會將下方文字編輯框的所有內容寫入檔案。過程會解決文字亂碼問題,教導字型與編碼設定;若未正確設定,非英文字元會變成問號亂碼。

點擊儲存時會彈出系統檔案儲存視窗,使用者可選擇儲存資料夾、自訂檔案名稱,確認後完成文字存檔。

下方是程式執行完成後的畫面截圖:

功能仿照Windows內建記事本,具備基礎文字編輯與存檔功能,編譯後執行檔體積非常輕巧。

以Release模式編譯後,程式僅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)
{
    // Windows常見繁簡中文字型路徑
    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();
        // 修復問號亂碼核心:傳入固定緩衝區最大長度,記憶體已預填零無殘值
        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)

我已經將核心邏輯封裝成獨立函式,上方分別是檔案寫入、系統檔案對話視窗、字型編碼設定三個功能函式。

介面繪製和之前範例相比,新增兩大元件:選單列與多行文字編輯框。

選單列實作

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

選單繪製起點在這裡,外層if透過ImGui::BeginMenu(“檔案”)建立第一層主選單。

內部放置可點擊的子選項,下方是ImGui官方標準範例:

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();
// 修復問號亂碼核心:傳入固定緩衝區最大長度,記憶體已預填零無殘值
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(“儲存成功”)註冊彈窗ID標記。

下方依據註冊的ID標記繪製模態提示視窗:

if (ImGui::BeginPopupModal("儲存成功", nullptr, ImGuiWindowFlags_AlwaysAutoResize))
{
    ImGui::Text("文字檔儲存完成!");
    if (ImGui::Button("確定", ImVec2(120, 0)))
        ImGui::CloseCurrentPopup();
    ImGui::EndPopup();
}Code language: PHP (php)

本課程內容到此結束。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *