雖然 .NET 提供 ConfigurationManager (app.config/web.config) 與 IConfiguration (ASP.NET Core),但在舊系統遷移、跨語言共用設定、無框架獨立部署場景下,INI 檔案仍具不可替代性。本文系列第一篇,深度解析 Windows API WritePrivateProfileString/GetPrivateProfileString 簽名、參數語義、C# P/Invoke 宣告細節與記憶體管理。
[Section1]
Key1=Value1
Key2=Value2
[Section2]
Key3=Value3
; 註解行以 ; 或 # 開頭
[Name] 標記Key=Value,等號前後空格會被保留為值一部分
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool WritePrivateProfileString(
string lpAppName, // 區段名稱 (Section),null 刪除整個區段
string lpKeyName, // 鍵名稱,null 刪除整個區段下的所有鍵
string lpString, // 值字串,null 刪除指定鍵
string lpFileName); // INI 檔案完整路徑
返回值:非零 = 成功,零 = 失敗 (呼叫 Marshal.GetLastWin32Error() 取得錯誤碼)。
特殊用法:
lpKeyName=null, lpString=null → 刪除 lpAppName 區段lpString=null → 刪除 lpAppName 區段下的 lpKeyName 鍵
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern uint GetPrivateProfileString(
string lpAppName, // 區段名稱
string lpKeyName, // 鍵名稱,null 讀取整個區段 (格式: key1=val1\0key2=val2\0\0)
string lpDefault, // 找不到時的預設值
StringBuilder lpReturnedString, // 接收緩衝區 (必須預先分配容量)
uint nSize, // 緩衝區容量 (字元數,含結尾 \0)
string lpFileName); // INI 檔案完整路徑
返回值:複製到緩衝區的字元數 (不含結尾 \0)。若返回 nSize - 1 表示緩衝區不足,需擴大重試。
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int GetPrivateProfileInt(
string lpAppName, string lpKeyName, int nDefault, string lpFileName);
直接回傳整數,失敗回傳 nDefault,無需緩衝區處理。
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool GetPrivateProfileStruct(
string lpAppName, string lpKeyName, IntPtr lpStruct, uint uSizeStruct, string lpFileName);
將區段資料直接解碼至 struct (需 StructLayout),適合二進位設定塊。
| CharSet | 行為 | 適用場景 |
|---|---|---|
| CharSet.Auto | Unicode 平台 → Unicode (W),ANSI 平台 → ANSI (A) | 推薦:跨平台相容 (Windows 9x 已淘汰) |
| CharSet.Unicode | 強制呼叫 W 版 (lpFileNameW 等) | 現代 Windows 專用,效能最佳 |
| CharSet.Ansi | 強制呼叫 A 版 | 舊系統相容,需手動編碼轉換 |
建議:CharSet.Unicode + 確保 INI 檔案為 UTF-16 LE (含 BOM),避免編碼亂碼。
var sb = new StringBuilder(256); // 預分配 256 字元
uint len = GetPrivateProfileString(section, key, "", sb, (uint)sb.Capacity, path);
if (len == sb.Capacity - 1) // 緩衝區不足
{
sb.Capacity = (int)(len * 2);
len = GetPrivateProfileString(section, key, "", sb, (uint)sb.Capacity, path);
}
string value = sb.ToString();
GetCurrentDirectory 為基準 (易變)Path.GetFullPath 正規化
using System;
using System.Runtime.InteropServices;
using System.Text;
public sealed class IniFile : IDisposable
{
private readonly string _path;
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern bool WritePrivateProfileString(string section, string key, string value, string filePath);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern uint GetPrivateProfileString(string section, string key, string defaultValue, StringBuilder retVal, uint size, string filePath);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetPrivateProfileInt(string section, string key, int defaultValue, string filePath);
public IniFile(string path) => _path = Path.GetFullPath(path);
public void Write(string section, string key, string value)
{
if (!WritePrivateProfileString(section, key, value, _path))
ThrowWin32Error("WritePrivateProfileString");
}
public string Read(string section, string key, string defaultValue = "")
{
var sb = new StringBuilder(256);
uint len = GetPrivateProfileString(section, key, defaultValue, sb, (uint)sb.Capacity, _path);
if (len == sb.Capacity - 1)
{
sb.Capacity = (int)(len * 2);
GetPrivateProfileString(section, key, defaultValue, sb, (uint)sb.Capacity, _path);
}
return sb.ToString();
}
public int ReadInt(string section, string key, int defaultValue = 0)
=> GetPrivateProfileInt(section, key, defaultValue, _path);
public void DeleteKey(string section, string key)
=> WritePrivateProfileString(section, key, null, _path);
public void DeleteSection(string section)
=> WritePrivateProfileString(section, null, null, _path);
public void Dispose() { /* 可選:快取同步到磁碟 */ }
private static void ThrowWin32Error(string api)
{
int err = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(err, $"{api} failed: 0x{err:X8}");
}
}
| 現象 | 原因 | 解決 |
|---|---|---|
| 中文讀出亂碼 | INI 為 ANSI (CP950/CP936),API 以 Unicode 讀取 | CharSet.Ansi + Encoding.GetEncoding(950).GetString/GetBytes |
| 寫入中文變問號 | CharSet.Unicode 寫入 UTF-16,但其他 ANSI 程式讀取 | 統一全系統用 UTF-16 LE INI,或雙版本 API 封裝 |
| 路徑含空格/中文失敗 | 舊版 ANSI API 路徑長度限制 260 (MAX_PATH) | 使用 \\?\ 前綴長路徑,或移至短路徑 |
下一篇將實作高階封裝:強型別繫結、區段對應類別、自動同步、多執行緒安全、ASP.NET Core IConfigurationProvider 整合,讓 INI 操作像 appsettings.json 一樣優雅。
結語:P/Invoke 讀寫 INI 的核心在於正確宣告 API 簽名、CharSet 與編碼一致、StringBuilder 緩衝區動態擴容、絕對路徑與錯誤碼檢查。掌握這些細節,即可構建穩健的 INI 存取層,為上層應用提供可靠基礎。