本文補充按鈕點擊事件的完整邏輯代碼 (對應設計器中的 btnBrowse_Click、btnWrite_Click、btnRead_Click),包含輸入驗證、P/Invoke 呼叫、緩衝區擴容、狀態標籤更新、異常捕獲與友好提示。可直接貼入 IniForm.cs 完成邏輯層。
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
namespace IniFileDemo
{
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);
public IniForm()
{
InitializeComponent();
}
// 選擇 INI 檔案
private void btnBrowse_Click(object sender, EventArgs e)
{
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
txtFilePath.Text = openFileDialog1.FileName;
UpdateStatus("已選擇檔案:" + openFileDialog1.FileName, false);
}
}
// 寫入 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; }
// 呼叫 Windows API
bool success = WritePrivateProfileString(section, key, value, path);
if (!success)
ThrowWin32Error("WritePrivateProfileString");
UpdateStatus($"寫入成功:[{section}] {key} = {(string.IsNullOrEmpty(value) ? "(空值)" : value)}", false);
}
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);
const string notFoundMarker = "【讀取失敗:找不到鍵】";
uint len = GetPrivateProfileString(section, key, notFoundMarker, sb, (uint)sb.Capacity, path);
// 緩衝區不足 → 擴容重讀
if (len == sb.Capacity - 1)
{
sb.Capacity = (int)(len * 2);
len = GetPrivateProfileString(section, key, notFoundMarker, sb, (uint)sb.Capacity, path);
}
string value = sb.ToString();
txtValue.Text = value;
if (value == notFoundMarker)
{
UpdateStatus("鍵不存在,已顯示預設提示", true);
txtValue.Text = ""; // 清空輸入框
}
else
{
UpdateStatus("讀取成功:" + value, false);
}
}
catch (Exception ex)
{
ShowError("讀取失敗:" + ex.Message);
}
}
// 狀態更新統一方法
private void UpdateStatus(string message, bool isError)
{
lblStatus.Text = message;
lblStatus.ForeColor = isError ? Color.Red : Color.Green;
}
private void ShowError(string message)
{
UpdateStatus(message, true);
MessageBox.Show(message, "錯誤", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
private static void ThrowWin32Error(string api)
{
int err = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(err, $"{api} 失敗 (錯誤碼: 0x{err:X8})");
}
}
}
btnWrite_Click
│
├─ 驗證路徑非空
├─ 驗證區段/鍵非空
├─ 取得值 (允許空字串)
├─ WritePrivateProfileString(section, key, value, path)
│ │
│ ├─ 成功 (true) → UpdateStatus(綠字)
│ └─ 失敗 (false) → GetLastWin32Error → Throw Win32Exception
└─ 捕獲 Exception → ShowError(紅字 + MessageBox)
btnRead_Click
│
├─ 驗證路徑/區段/鍵非空
├─ StringBuilder(256)
├─ GetPrivateProfileString(section, key, "【不存在標記】", sb, 256, path)
│ │
│ ├─ len < 255 → 正常返回
│ └─ len == 255 → sb.Capacity *= 2 → 重呼 API
├─ value = sb.ToString()
│ │
│ ├─ value == "【不存在標記】" → txtValue清空、UpdateStatus(黃/紅字)
│ └─ 正常值 → txtValue顯示、UpdateStatus(綠字)
└─ 捕獲 Exception → ShowError
| 操作 | API 參數 | INI 結果 | 讀回值 |
|---|---|---|---|
| 寫入空字串 | value = "" | key= (等號後無字元) | 空字串 "" |
| 刪除鍵 | value = null | 整行移除 | 預設值 / 不存在標記 |
| 刪除區段 | key = null, value = null | 整區段移除 | 預設值 |
本範例 txtValue.Text 回傳空字串時傳 "",保留鍵;若需刪除鍵,可加「刪除鍵」按鈕傳 null。
len = 複製字元數 (不含結尾 \0)Cap = 可容納字元數 (含 \0)len == Cap - 1 → 緩衝區剛好滿 (只剩 1 格給 \0) → 擴容Cap = len * 2 (指數增長,極少需二次擴容)txtValue,所見即所得
try
{
// P/Invoke 呼叫
}
catch (Win32Exception w32) // 捕獲 ThrowWin32Error 拋出的
{
ShowError($"系統錯誤 (0x{w32.NativeErrorCode:X8}):{w32.Message}");
}
catch (UnauthorizedAccessException)
{
ShowError("權限不足:無法存取檔案,請檢查唯讀屬性或管理員權限");
}
catch (Exception ex) // 兜底
{
ShowError("未預期錯誤:" + ex.Message);
}
[TestMethod]
public void Write_ThenRead_RoundTrip()
{
string tempIni = Path.GetTempFileName() + ".ini";
try
{
// 寫入
Assert.IsTrue(WritePrivateProfileString("Test", "Key1", "Value1", tempIni));
Assert.IsTrue(WritePrivateProfileString("Test", "Key2", "", tempIni)); // 空值
Assert.IsTrue(WritePrivateProfileString("Test", "Key3", null, tempIni)); // 刪除
// 讀取驗證
var sb = new StringBuilder(256);
GetPrivateProfileString("Test", "Key1", "DEF", sb, 256, tempIni);
Assert.AreEqual("Value1", sb.ToString());
GetPrivateProfileString("Test", "Key2", "DEF", sb, 256, tempIni);
Assert.AreEqual("", sb.ToString()); // 空值非預設值
GetPrivateProfileString("Test", "Key3", "DEF", sb, 256, tempIni);
Assert.AreEqual("DEF", sb.ToString()); // 已刪除 → 預設值
}
finally
{
File.Delete(tempIni);
}
}
x64 (64位元 OS) 或 Any CPU (Prefer 32-bit 取消勾選)Program Files、Windows 等受保護目錄,需 app.manifest 設 requireAdministratorkernel32.dll 寫入行為,需加入白名單結語:按鈕事件邏輯已完整封裝,涵蓋驗證、呼叫、擴容、回饋、異常五大環節。這套模式可直接複用至任何 P/Invoke 場景。下一篇將實作強型別封裝:IniFile.Bind<T>()、屬性對應、自動同步、ASP.NET Core 整合,徹底告別字串鍵值的魔術字串。