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

C# 讀寫 INI 檔案完整指南 (四):按鈕事件完整邏輯與狀態回饋

     編輯:關於C語言

本文補充按鈕點擊事件的完整邏輯代碼 (對應設計器中的 btnBrowse_Click、btnWrite_Click、btnRead_Click),包含輸入驗證、P/Invoke 呼叫、緩衝區擴容、狀態標籤更新、異常捕獲與友好提示。可直接貼入 IniForm.cs 完成邏輯層。

完整事件處理代碼 (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

關鍵細節深度解析

1. 空值語義區分

操作API 參數INI 結果讀回值
寫入空字串value = ""key= (等號後無字元)空字串 ""
刪除鍵value = null整行移除預設值 / 不存在標記
刪除區段key = null, value = null整區段移除預設值

本範例 txtValue.Text 回傳空字串時傳 "",保留鍵;若需刪除鍵,可加「刪除鍵」按鈕傳 null。

2. 緩衝區擴容的數學原理

  • API 返回 len = 複製字元數 (不含結尾 \0)
  • 緩衝區容量 Cap = 可容納字元數 (含 \0)
  • 若 len == Cap - 1 → 緩衝區剛好滿 (只剩 1 格給 \0) → 擴容
  • 擴容策略:Cap = len * 2 (指數增長,極少需二次擴容)

3. 狀態回饋 UX 設計

  • 標籤顏色:綠=成功、紅=錯誤、橙=警告 (如鍵不存在)
  • MessageBox:僅錯誤時彈出,成功靜默 (避免干擾流程)
  • 即時顯示:讀取後自動填入 txtValue,所見即所得

4. 異常處理層級


try
{
    // P/Invoke 呼叫
}
catch (Win32Exception w32) // 捕獲 ThrowWin32Error 拋出的
{
    ShowError($"系統錯誤 (0x{w32.NativeErrorCode:X8}):{w32.Message}");
}
catch (UnauthorizedAccessException)
{
    ShowError("權限不足:無法存取檔案,請檢查唯讀屬性或管理員權限");
}
catch (Exception ex) // 兜底
{
    ShowError("未預期錯誤:" + ex.Message);
}

單元測試建議 (MSTest/xUnit)


[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);
    }
}

部署注意事項

  • 目標平台:專案屬性 → Build → Platform target 設 x64 (64位元 OS) 或 Any CPU (Prefer 32-bit 取消勾選)
  • 管理員權限:若 INI 放在 Program Files、Windows 等受保護目錄,需 app.manifest 設 requireAdministrator
  • 防毒軟體攔截:部分防毒軟體攔截 kernel32.dll 寫入行為,需加入白名單

結語:按鈕事件邏輯已完整封裝,涵蓋驗證、呼叫、擴容、回饋、異常五大環節。這套模式可直接複用至任何 P/Invoke 場景。下一篇將實作強型別封裝:IniFile.Bind<T>()、屬性對應、自動同步、ASP.NET Core 整合,徹底告別字串鍵值的魔術字串。

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