首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 讀寫 INI 檔案完整指南 (一):P/Invoke 核心 API 與結構解析

     編輯:關於C語言

雖然 .NET 提供 ConfigurationManager (app.config/web.config) 與 IConfiguration (ASP.NET Core),但在舊系統遷移、跨語言共用設定、無框架獨立部署場景下,INI 檔案仍具不可替代性。本文系列第一篇,深度解析 Windows API WritePrivateProfileString/GetPrivateProfileString 簽名、參數語義、C# P/Invoke 宣告細節與記憶體管理。

INI 檔案結構回顧


[Section1]
Key1=Value1
Key2=Value2

[Section2]
Key3=Value3
; 註解行以 ; 或 # 開頭
  • 區段 (Section) 以 [Name] 標記
  • 鍵值對:Key=Value,等號前後空格會被保留為值一部分
  • 檔案編碼:通常為 ANSI (系統預設代碼頁) 或 UTF-16 LE (含 BOM)

核心 Windows API 簽名 (kernel32.dll)

1. 寫入:WritePrivateProfileString


[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 鍵

2. 讀取:GetPrivateProfileString


[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 表示緩衝區不足,需擴大重試。

3. 讀取整數:GetPrivateProfileInt


[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern int GetPrivateProfileInt(
    string lpAppName, string lpKeyName, int nDefault, string lpFileName);

直接回傳整數,失敗回傳 nDefault,無需緩衝區處理。

4. 讀取結構體:GetPrivateProfileStruct (較少用)


[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool GetPrivateProfileStruct(
    string lpAppName, string lpKeyName, IntPtr lpStruct, uint uSizeStruct, string lpFileName);

將區段資料直接解碼至 struct (需 StructLayout),適合二進位設定塊。

C# P/Invoke 宣告最佳實踐

CharSet 選擇

CharSet行為適用場景
CharSet.AutoUnicode 平台 → Unicode (W),ANSI 平台 → ANSI (A)推薦:跨平台相容 (Windows 9x 已淘汰)
CharSet.Unicode強制呼叫 W 版 (lpFileNameW 等)現代 Windows 專用,效能最佳
CharSet.Ansi強制呼叫 A 版舊系統相容,需手動編碼轉換

建議:CharSet.Unicode + 確保 INI 檔案為 UTF-16 LE (含 BOM),避免編碼亂碼。

StringBuilder 緩衝區管理


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 正規化
  • 若檔案不存在,API 會嘗試建立 (需目錄寫入權限)

完整封裝類別骨架


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 存取層,為上層應用提供可靠基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved