承接上篇核心 API,本文聚焦WinForms 實戰場景:介面佈局、按鈕事件處理、路徑選擇、讀寫流程與異常處理。提供可直接編譯執行的完整表單代碼,展示「選擇檔案 → 輸入區段/鍵/值 → 寫入/讀取 → 回饋結果」的標準交互閉環。
| 控制項 | Name | Text/屬性 | 用途 |
|---|---|---|---|
| TextBox | txtFilePath | ReadOnly=true | 顯示 INI 完整路徑 |
| Button | btnBrowse | 「選擇 INI 檔案」 | 開啟 OpenFileDialog |
| TextBox | txtSection | 輸入區段名稱 | |
| TextBox | txtKey | 輸入鍵名稱 | |
| TextBox | txtValue | 輸入/顯示值 | |
| Button | btnWrite | 「寫入 INI」 | 觸發寫入 |
| Button | btnRead | 「讀取 INI」 | 觸發讀取 |
| Label | lblStatus | AutoSize=true | 狀態訊息 |
private readonly OpenFileDialog _openDlg = new OpenFileDialog
{
Filter = "INI 檔案 (*.ini)|*.ini|所有檔案 (*.*)|*.*",
Title = "選擇或建立 INI 設定檔",
CheckFileExists = false, // 允許輸入新檔名
AddExtension = true,
DefaultExt = "ini"
};
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
public partial class IniForm : Form
{
// P/Invoke 宣告 (同上篇)
[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);
private readonly OpenFileDialog _openDlg = new OpenFileDialog
{
Filter = "INI Files (*.ini)|*.ini|All Files (*.*)|*.*",
CheckFileExists = false,
AddExtension = true,
DefaultExt = "ini"
};
public IniForm()
{
InitializeComponent(); // 設計器生成的控制項初始化
}
// 選擇檔案
private void btnBrowse_Click(object sender, EventArgs e)
{
if (_openDlg.ShowDialog() == DialogResult.OK)
txtFilePath.Text = _openDlg.FileName;
}
// 寫入 INI
private void btnWrite_Click(object sender, EventArgs e)
{
try
{
string path = txtFilePath.Text.Trim();
if (string.IsNullOrEmpty(path))
{ ShowError("請先選擇 INI 檔案路徑"); return; }
string section = txtSection.Text.Trim();
string key = txtKey.Text.Trim();
string value = txtValue.Text; // 值可為空字串
if (string.IsNullOrEmpty(section) || string.IsNullOrEmpty(key))
{ ShowError("區段與鍵名稱不可為空"); return; }
if (!WritePrivateProfileString(section, key, value, path))
ThrowWin32Error("WritePrivateProfileString");
ShowSuccess($"成功寫入:[{section}] {key} = {value}");
}
catch (Exception ex)
{
ShowError($"寫入失敗:{ex.Message}");
}
}
// 讀取 INI
private void btnRead_Click(object sender, EventArgs e)
{
try
{
string path = txtFilePath.Text.Trim();
if (string.IsNullOrEmpty(path))
{ ShowError("請先選擇 INI 檔案路徑"); return; }
string section = txtSection.Text.Trim();
string key = txtKey.Text.Trim();
if (string.IsNullOrEmpty(section) || string.IsNullOrEmpty(key))
{ ShowError("區段與鍵名稱不可為空"); return; }
var sb = new StringBuilder(256);
uint len = GetPrivateProfileString(section, key, "【讀取失敗:找不到鍵】", sb, (uint)sb.Capacity, path);
// 緩衝區不足自動擴容
if (len == sb.Capacity - 1)
{
sb.Capacity = (int)(len * 2);
GetPrivateProfileString(section, key, "【讀取失敗:找不到鍵】", sb, (uint)sb.Capacity, path);
}
string value = sb.ToString();
txtValue.Text = value;
ShowSuccess(value == "【讀取失敗:找不到鍵】" ? "鍵不存在,已顯示預設值" : $"讀取成功:{value}");
}
catch (Exception ex)
{
ShowError($"讀取失敗:{ex.Message}");
}
}
private void ShowSuccess(string msg)
{
lblStatus.Text = msg;
lblStatus.ForeColor = Color.Green;
}
private void ShowError(string msg)
{
lblStatus.Text = msg;
lblStatus.ForeColor = Color.Red;
MessageBox.Show(msg, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private static void ThrowWin32Error(string api)
{
int err = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(err, $"{api} failed (0x{err:X8})");
}
}
OpenFileDialog 保證合法路徑格式null 會刪除鍵,空字串保留鍵=空)
// 首次 256 字元
uint len = GetPrivateProfileString(..., sb, 256, ...);
// 若 len == 255 → 緩衝區滿 (保留 1 字元給 \0)
if (len == sb.Capacity - 1)
{
sb.Capacity = (int)(len * 2); // 雙倍擴容
GetPrivateProfileString(..., sb, (uint)sb.Capacity, ...);
}
避免長值被截斷,單次 API 呼叫即可完成 (極少需再次擴容)。
| 場景 | lpDefault 傳入 | 行為 |
|---|---|---|
| 鍵不存在 | "預設值" | 回傳 "預設值" |
| 鍵存在但值為空 | "預設值" | 回傳空字串 (非預設值) |
| 區段不存在 | "預設值" | 回傳 "預設值" |
本範例傳入明確標記字串 「【讀取失敗:找不到鍵】」,方便判斷是否命中預設值。
if (!WritePrivateProfileString(...))
ThrowWin32Error("WritePrivateProfileString"); // 拋 Win32Exception 含錯誤碼
常見錯誤碼:
0x00000002 (ERROR_FILE_NOT_FOUND):目錄不存在0x00000005 (ERROR_ACCESS_DENIED):唯讀/權限不足0x00000020 (ERROR_SHARING_VIOLATION):檔案被其他進程獨佔C:\Temp\test.ini (目錄需存在)Database,鍵:ConnectionString,值:Server=.;Database=Test;Uid=sa;Pwd=123 → 點「寫入 INI」Database,鍵留空 → 呼叫 WritePrivateProfileString("Database", null, null, path) → 整區段消失FolderBrowserDialog + 檔名輸入框,避免手動輸入錯誤GetPrivateProfileString(null, null, ...)) 供選擇lock (_sync) 包裹讀寫,防止並發損壞File.Copy(path, path + ".bak", true)CharSet.Ansi/Unicode結語:WinForms 操作 INI 的核心流程已完整呈現。關鍵在於輸入驗證、緩衝區擴容、預設值語義區分、Win32 錯誤碼轉換。下一篇將封裝為強型別設定類別,實現 IniConfig.Bind<T>() 體驗,並整合進 ASP.NET Core IConfiguration 體系。