INIReader.h 解析:INI 格式解析器
本篇定位
這是 Phase 7 的第 2 篇。
INIReader.h是一個輕量的 C++ 單頭標頭檔 INI 解析器,讓MainMain::LoadConfig()能用幾行程式碼讀取dinput8.ini的所有設定。
一、什麼是 INI 格式?
INI 是一種極簡單的設定檔格式,由 Windows 3.x 時代流傳下來,規則只有三條:
[Section] ; 1. 方括號包住的是「區段(Section)」名稱
Key = Value ; 2. 等號左邊是鍵名,右邊是值
; 這是注釋 ; 3. 分號開頭的行是注釋,忽略範例:
[Connection]
ServerIP = 127.0.0.1
Port = 8484二、為什麼不用 Windows 內建的 GetPrivateProfileString?
Windows API 有 GetPrivateProfileString / GetPrivateProfileInt 可以讀取 INI 檔案,但有幾個問題:
| Windows API | INIReader | |
|---|---|---|
| 路徑限制 | 需要絕對路徑 | 接受相對路徑 |
| 編碼 | ANSI 或 Unicode 二選一 | 自動處理 |
| 型別支援 | 只有字串和整數 | 另外支援布林、浮點數 |
| 可攜性 | 僅 Windows | 跨平台(在 Linux/Mac 上編譯也能用) |
| 標頭依賴 | 需要 <windows.h> | 僅需 C++ 標準庫 |
三、INIReader.h 的完整實作
#pragma once
#include <string>
#include <map>
#include <fstream>
#include <sstream>
#include <algorithm>
#include <cctype>
class INIReader {
public:
// 建構時立刻解析整個檔案
explicit INIReader(const std::string& filename) {
ParseFile(filename);
}
// 讀取字串值(找不到時回傳 defaultValue)
std::string Get(const std::string& section,
const std::string& key,
const std::string& defaultValue) const
{
std::string lookupKey = MakeKey(section, key);
auto it = m_values.find(lookupKey);
return (it != m_values.end()) ? it->second : defaultValue;
}
// 讀取整數值
long GetInteger(const std::string& section,
const std::string& key,
long defaultValue) const
{
std::string val = Get(section, key, "");
if (val.empty()) return defaultValue;
try { return std::stol(val); }
catch (...) { return defaultValue; }
}
// 讀取浮點數值
double GetReal(const std::string& section,
const std::string& key,
double defaultValue) const
{
std::string val = Get(section, key, "");
if (val.empty()) return defaultValue;
try { return std::stod(val); }
catch (...) { return defaultValue; }
}
// 讀取布林值("true", "yes", "1", "on" 視為 true)
bool GetBoolean(const std::string& section,
const std::string& key,
bool defaultValue) const
{
std::string val = Get(section, key, "");
if (val.empty()) return defaultValue;
std::string lower = ToLower(val);
if (lower == "true" || lower == "yes" || lower == "1" || lower == "on")
return true;
if (lower == "false" || lower == "no" || lower == "0" || lower == "off")
return false;
return defaultValue;
}
// 取得解析錯誤的描述(空字串 = 無錯誤)
std::string ParseError() const { return m_parseError; }
private:
std::map<std::string, std::string> m_values;
std::string m_parseError;
// ── 解析核心 ────────────────────────────────────────────────────────────
void ParseFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
m_parseError = "Cannot open file: " + filename;
return;
}
std::string currentSection;
std::string line;
int lineNum = 0;
while (std::getline(file, line)) {
lineNum++;
line = Trim(line); // 去掉前後空白
// 跳過空行和注釋
if (line.empty() || line[0] == ';' || line[0] == '#')
continue;
// 區段標頭 [SectionName]
if (line[0] == '[') {
size_t end = line.find(']');
if (end == std::string::npos) {
m_parseError = "Malformed section at line " + std::to_string(lineNum);
return;
}
currentSection = Trim(line.substr(1, end - 1));
continue;
}
// 鍵值對 Key = Value
size_t eqPos = line.find('=');
if (eqPos == std::string::npos) continue; // 沒有等號的行,忽略
std::string key = Trim(line.substr(0, eqPos));
std::string value = Trim(line.substr(eqPos + 1));
// 移除行內注釋(= 右邊的 ; 後面都是注釋)
size_t commentPos = value.find(';');
if (commentPos != std::string::npos)
value = Trim(value.substr(0, commentPos));
m_values[MakeKey(currentSection, key)] = value;
}
}
// ── 輔助函式 ────────────────────────────────────────────────────────────
// 把 section + key 組成唯一鍵(小寫,不區分大小寫)
static std::string MakeKey(const std::string& section, const std::string& key) {
return ToLower(section) + "." + ToLower(key);
}
// 轉小寫
static std::string ToLower(std::string s) {
std::transform(s.begin(), s.end(), s.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
return s;
}
// 去掉字串前後的空白字元
static std::string Trim(const std::string& s) {
const char* ws = " \t\r\n";
size_t start = s.find_first_not_of(ws);
if (start == std::string::npos) return "";
size_t end = s.find_last_not_of(ws);
return s.substr(start, end - start + 1);
}
};四、資料流視覺化
dinput8.ini 檔案內容:
[Connection]
ServerIP = 127.0.0.1 ; 本機
Port = 8484
ParseFile() 解析後的 m_values(std::map):
"connection.serverip" → "127.0.0.1"
"connection.port" → "8484"
Get("Connection", "ServerIP", "") 呼叫流程:
MakeKey("Connection", "ServerIP") → "connection.serverip"
m_values.find("connection.serverip") → 找到 "127.0.0.1"
回傳 "127.0.0.1"
為什麼要轉小寫?
INI 格式傳統上不區分大小寫(
[Connection]和[connection]應該是同一區段)。MakeKey把 section 和 key 都轉小寫,這樣Get("connection", "serverip", "")和Get("Connection", "ServerIP", "")會查到同一個值。
五、行內注釋的處理
ServerIP = 127.0.0.1 ; 這是行內注釋,不是 IP 的一部分ParseFile 在讀到值之後,額外搜尋 ; 並把它後面的內容截掉:
size_t commentPos = value.find(';');
if (commentPos != std::string::npos)
value = Trim(value.substr(0, commentPos));結果:value 從 "127.0.0.1 ; 這是行內注釋" 變成 "127.0.0.1"。
如果值本身包含分號
例如
Password = abc;def會被截成abc,這是 INI 格式的已知限制。如果需要支援值中含有;,需要改用引號包圍(Password = "abc;def")並修改解析邏輯。在 MapleEzorsia-v2 的使用情境中,所有設定值(IP、Port、數字、布林)都不含分號,不需要處理這個邊界情況。
六、常見問題
Q:整個 INIReader 都在 .h 裡,不需要 .cpp 嗎?
是的。這是「單頭標頭庫(Header-only library)」的設計模式。把所有程式碼放在
.h裡,使用方只需要#include "INIReader.h",不需要編譯額外的.cpp。缺點是每個 include 這個標頭的翻譯單元(.cpp)都會編譯一份,增加編譯時間;優點是整合非常簡單。
Q:
std::mapvsstd::unordered_map,哪個更好?
std::unordered_map的平均查詢時間是 O(1),std::map是 O(log n)。但設定檔通常只有幾十個鍵,這個差距微不足道。std::map按字母排序,方便除錯時列出所有鍵值對,所以本實作選它。
延伸閱讀
- 01_config.ini設定系統 — 上一篇:設定檔的格式與所有選項
- 03_MainMain.cpp_初始化流程解析 — INIReader 在 LoadConfig 中的使用方式
- 03_WZ與IMG資源整合原理 — 下一篇:WZ 資源系統與登入器的整合