C#成长之路1 | INI文件读写与遍历所有节点Section(增删改查)

一、INI文件介绍

1.背景

INI文件是一种配置文件格式,是Initialization file的缩写,即为初始化文件,通
常用于Windows操作系统中的应用程序中。虽然INI是远古时期的产物,但它
依然能屹立在这高手纵云(XML、YAML、JSON…)的年代。

2.格式

INI结构非常简单文件是一种易于编辑和阅读的文本,由节(Section)、键(key)、值(value)、注释(comments)组成的两层结构。键值对用”=“隔开,左边为键,右边为值,每个键值对都要归纳于段落中,段落必须写在”[]“里。注释则以分号”;“开头即可,如下:

[参数1]
;记录参数1的键1值
键1=12=23=3
[参数2]1=12=23=3

3.后缀(文件扩展名)

.ini、.cfg、.conf、.txt

4.特点:

优点:

  1. 结构简单,两层结构一目了然,符合人类理解事物的逻辑,易于维护;
  2. 读写速度快,适合作为软件系统各模块的配置表,减少软件启动时间;
  3. 接口简单,上手容易,对新手友好。

缺点:

  1. 结构过于简单,只有两层结构,不适合描绘复杂类型数据、多级数据;
  2. 大小限制64kb;
  3. 操作不当,中文描绘数据变乱码;
  4. 只支持字符串类型;

二、增删改

1.API介绍(参考官方文档

Kernel32.dll(Windows平台下必有的动态库文件)

操作INI文件需要调用系统底层的API,而这些API封装在Kernel32.dll中。
它是一个Windows操作系统的核心动态链接库文件,位于Windows系统目录下,并提供了大量的API函数用于读写文件、管理内存、管理线程,如:创建、打开、读写、关闭文件、线程管理、进程管理、调试、错误处理、时间处理。

GetPrivateProfileInt(通过节、键,获取文件int数值)

  • 原函数:
UINT GetPrivateProfileInt(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
INT nDefault, 
LPCTSTR lpFileName)
  • 参数:
lpAppName: 节名称,注意这个字串是不区分大小写的。
lpKeyName: 键名称,这个支持不区分大小写。
nDefault:  默认值,指定条目未找到时返回的默认值。
lpFileName:初始化文件的名字,如果没有指定完整的路径名,windows就会在Windows目录中搜索文件。
  • 返回值
返回值是指定初始化文件中指定键名称后面的字符串的整数等效项。 如果未找到键,则返回值为指定的默认值。
  • C#声明方法
[DllImport("kernel32.dll")]
private static extern int GetPrivateProfileInt(string lpAppName,string lpKeyName,int nDefault,string lpFileName);

GetPrivateProfileString(通过节、键,获取文件字符串)

  • 原函数:
DWORD GetPrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpDefault,
LPCTSTR  lpReturnedString,
DWORD   nSize,
LPC TSTR lpFileName
);
  • 参数:
lpAppName:       节名称,注意这个字串是不区分大小写的。如果此参数为 NULL,则读取所有节点名。
lpKeyName:       键名称,这个支持不区分大小写。如果此参数为 NULL,lpAppName不为null,则读取所有该节点的所有键值。
lpDefault:       默认值,指定条目未找到时返回的默认值。
lpReturnedString:存储返回值,指向接收检索字符串的缓冲区的指针。
nSize:           参数指向的缓冲区的大小(以字符为单位)。
lpFileName:      初始化文件的名字。如果没有指定完整的路径名,windows就会在Windows目录中搜索文件
  • 返回值
返回值是复制到缓冲区的字符数,不包括终止 null 字符。

如果 lpAppName 和 lpKeyName 都不是 NULL ,并且提供的目标缓冲区太小,无法保存请求的字符串,则字符串将被截断,后跟 null 字符,并且返回值等于 nSize 减一。

如果 lpAppName 或 lpKeyName 为 NULL ,并且提供的目标缓冲区太小,无法容纳所有字符串,则最后一个字符串将被截断,后跟两个 null 字符。 在这种情况下,返回值等于 nSize 减 2。

如果找不到 lpFileName 指定的初始化文件或包含无效值,则此函数会将 errorno 设置为“0x2”, (找不到文件) 。 若要检索扩展的错误信息,请调用 GetLastError。
  • C#声明方法
/// <summary>直接获得字符串类型</summary>
[DllImport("kernel32")]
private static extern int GetPrivateProfileString(string section, string key, string def, StringBuilder retVal, int size, string filePath);

为了方便后续遍历,声明一个重载函数来获得字符串的二进制

/// <summary>获取字符串类型对应的字节</summary>
[DllImport("kernel32")]
private static extern int GetPrivateProfileString(string section, string key, string def, byte[] retVal, int size, string filePath);

getPrivateProfileSection(通过节,获取节下面的所有键和值)

  • 原函数:
DWORD GetPrivateProfileSection(
  LPCTSTR lpAppName,
  LPTSTR  lpReturnedString,
  DWORD   nSize,
  LPCTSTR lpFileName
);
  • 参数:
lpAppName:       节名称,注意这个字串是不区分大小写的。
lpReturnedString:存储返回值,指向接收键名称和值的缓冲区的指针。 缓冲区用一个或多个以 null 结尾的字符串填充;最后一个字符串后跟第二个 null 字符。
nSize:           指向的缓冲区的大小(以字符为单位)。最大配置文件节大小为 32,767 个字符。
lpFileName:      初始化文件的名字。如果没有指定完整的路径名,windows就会在Windows目录中搜索文件
  • 返回值
返回值指定复制到缓冲区的字符数,不包括终止 null 字符。 如果缓冲区不够大,无法包含与命名节关联的所有键名称和值对,则返回值等于 nSize 减 2
  • C#声明方法
 /// <summary>获取节点下所有键</summary>
 [DllImport("kernel32.dll", EntryPoint = "GetPrivateProfileSection")]
 private static extern uint GetPrivateProfileSection(string lpAppName,sbyte[] lpReturnedString, int nSize, string lpFileName);

getPrivateProfileSectionNames(获文件中所有取节名称)

  • 原函数:
DWORD GetPrivateProfileSectionNames(
  LPTSTR  lpszReturnBuffer,
  DWORD   nSize,
  LPCTSTR lpFileName
);
  • 参数:
lpszReturnBuffer:存储返回值,指向接收与命名文件关联的节名称的缓冲区的指针。 缓冲区用一个或多个 以 null 结尾的字符串填充;最后一个字符串后跟第二个 null 字符。
nSize:           指向的缓冲区的大小(以字符为单位)。
lpFileName:      初始化文件的名字。如果没有指定完整的路径名,windows就会在Windows目录中搜索文件
  • 返回值
返回值指定复制到指定缓冲区的字符数,不包括终止 null 字符。 如果缓冲区不够大,无法包含与指定初始化文件关联的所有节名称,则返回值等于 nSize 减 2 指定的大小。
  • C#声明方法
 /// <summary>获取所有节点名称</summary>
[DllImport("kernel32.dll",EntryPoint = "GetPrivateProfileSectionNames")]
private static extern uint GetPrivateProfileSectionNames(sbyte[] lpszReturnBuffer, uint nSize, string lpFileName);

writePrivateProfileStringA(通过节、键,写入字符串值。)

  • 原函数:
BOOL WritePrivateProfileString(
LPCTSTR lpAppName,
LPCTSTR lpKeyName,
LPCTSTR lpString,
LPCTSTR lpFileName
);
  • 参数:
lpAppName: 节名称。注意这个字串是不区分大小写的
lpKeyName: 键名称。这个支持不区分大小写
lpString:  要写入文件的 以 null 结尾的字符串。 如果此参数为 NULL,则删除 lpKeyName 参数指向的键。
lpFileName:初始化文件的名称。如果文件是使用 Unicode 字符创建的,函数会将 Unicode 字符写入文件。 否则,该函数将写入 ANSI 字符。
  • 返回值
如果函数成功将字符串复制到初始化文件,则返回值为非零值。
如果函数失败,或者刷新最近访问的初始化文件的缓存版本,则返回值为零。 要获得更多的错误信息,请调用 GetLastError。

其他API不常用,这里就不列举,想了解的同学可以移步到官网查阅

2.API封装

  • 在声明AIPI后,为了便于调用,我们需要对API进行封装。
  • 读类API
 		/// <summary>
        /// 读Int数值
        /// </summary>
        /// <param name="section">节</param>
        /// <param name="name">键</param>
        /// <param name="def">默认值</param>
        /// <returns></returns>
        public int ReadInt(string section, string name, int def)
        {
    
    
            return GetPrivateProfileInt(section, name, def, this.Filepath);
        }
        
        /// <summary>
        /// 读取string字符串
        /// </summary>
        /// <param name="section">节</param>
        /// <param name="name">键</param>
        /// <param name="def">默认值</param>
        /// <returns></returns>
        public string ReadString(string section, string name, string def)
        {
    
    
            StringBuilder vRetSb = new StringBuilder(2048);
            GetPrivateProfileString(section, name, def, vRetSb, 2048, this.Filepath);
            return vRetSb.ToString();
        }

        /// <summary>
        /// 读取double
        /// </summary>
        /// <param name="section">节</param>
        /// <param name="name">键</param>
        /// <param name="def">默认值</param>
        /// <returns></returns>
        public double ReadDouble(string section, string name, double def)
        {
    
    
            StringBuilder vRetSb = new StringBuilder(2048);
            GetPrivateProfileString(section, name, "", vRetSb, 2048, this.Filepath);
            if (vRetSb.Length<1)
            {
    
    
                return def;
            }
            return Convert.ToDouble(vRetSb.ToString());
        }
  • 写类API
 /// <summary>
        /// [扩展]写入String字符串,如果不存在 节-键,则会自动创建
        /// </summary>
        /// <param name="section">节</param>
        /// <param name="name">键</param>
        /// <param name="strVal">写入值</param>
        public void WriteString(string section, string name, string strVal)
        {
    
    
            WritePrivateProfileString(section, name, strVal, this.Filepath);
        }

  • 删除类API
 /// <summary>
        /// 删除指定字段
        /// </summary>
        /// <param name="sectionName">段落</param>
        /// <param name="keyName">键</param>
        public void iniDelete(string sectionName, string keyName)
        {
    
    
            WritePrivateProfileString(sectionName, keyName, null, this.Filepath);
        }
        /// <summary>
        /// 删除字段重载
        /// </summary>
        /// <param name="sectionName">段落</param>
        public void iniDelete(string sectionName)
        {
    
    
            WritePrivateProfileString(sectionName, null, null, this.Filepath);
        }
        /// <summary>
        /// 删除ini文件下所有段落
        /// </summary>
        public void ClearAllSection()
        {
    
    
            WriteString(null, null, null);
        }
        /// <summary>
        /// 删除ini文件下personal段落下的所有键
        /// </summary>
        /// <param name="Section"></param>
        public void ClearSection(string Section)
        {
    
    
            WriteString(Section, null, null);
        }

3.结果展示

  • 篇幅有限,就不展示调用结果了。

三、遍历INI

1.使用场景

有人会问,既然用到遍历功能,为啥不用xml等其他文本呢?但我这里的场景,感觉还是使用INI文件比较合适。
使用INI配置库的名称,通过反射的方式给软件动态注入功能。当然INI优势就是快。
在这里插入图片描述
以上INI文件是记录库名称和是否启用该功能。当软件启动是,先遍历该INI文件,把说有dll都提取出来,通过dll名字反编译得到其类型,动态创建对象。

2.实现方法

遍历INI其实已经提供了专门的API,

  1. 使用getPrivateProfileSectionNames 函数 检索初始化文件中所有节的名称。
  2. 使用getPrivateProfileSection获取节下面的所有键和值。

我使用了另一种方法:

  1. 使用GetPrivateProfileString(null, null, “”, allSectionByte, 4096, this.Filepath)方式获取文件中所有节点。
  2. 需要传入一个byte[]数组用于接受遍历结果叠加的二进制,使用Encoding.GetEncoding()或者ASCIIEncoding类下的GetString()方法转成字符串后,每个节点之间默认使用‘\0’作为分隔符,如果我们直接使用或者打印,只能得到第一各节点,因为“\0”是结束符。
  3. 使用String.Replace()函数把”\0“替换成"|"。
  4. 使用String.Substring()函数把替换后的字符串进行分割到数组。
/// <summary>
        /// 读取所有段落名,写死读4096个大小字节,如果段落加起来字符过长会读不完整
        /// </summary>
        /// <returns>返回字符串数组,所有Section</returns>
        public string[] GetAllSections()
        {
    
    
            byte[] allSectionByte = new byte[4096];
            int length = GetPrivateProfileString(null, null, "", allSectionByte, 1024, this.Filepath);
            //int bufLen = GetPrivateProfileString(section, key, Default, Buffer, Buffer.GetUpperBound(0), INI_Path);
            //返回的是所有段落名称拼接起来,以“\0”为分割符
            string allSectionStr = Encoding.GetEncoding(Encoding.ASCII.CodePage).GetString(allSectionByte, 0, length);
            //用'|'代替‘\0’作为分隔符
            string allSectionStrEx = allSectionStr.Replace('\0', '|');
            //去除追后一个分割符"|",不然会有一个空数据
            allSectionStrEx = allSectionStrEx.Substring(0, length - 1);
            //以'|'为分隔符分割allSectionStrEx到字符串数组
            string[] allSections = new string[length];
            char[] separator = {
    
     '|' };
            allSections = allSectionStrEx.Split(separator);
            return allSections;
        }

  1. 同理可使用GetPrivateProfileString(section, null, “”, allSectionByte, 4096, this.Filepath);方式获取该节点的所有键。
         /// <summary>
        /// 获取段落下的所有键,写死读4096个大小字节,如果段落加起来字符过长会读不完整
        /// </summary>
        /// <param name="section">段落名</param>
        /// <returns>返回字符串数组,所有Key</returns>
        public string[] GetAllKeys(string section)
        {
    
    
            byte[] allSectionByte = new byte[4096];
            int length = GetPrivateProfileString(section, null, "", allSectionByte, 4096, this.Filepath);
            string allSectionStr = Encoding.GetEncoding(Encoding.ASCII.CodePage).GetString(allSectionByte, 0, length);
            //用'|'代替‘\0’作为分隔符
            string allSectionStrEx = allSectionStr.Replace('\0', '|');
            //去除追后一个分割符"|",不然会有一个空数据
            allSectionStrEx = allSectionStrEx.Substring(0, length - 1);
            //以'|'为分隔符分割allSectionStrEx到字符串数组
            string[] allSections = new string[length];
            char[] separator = {
    
     '|' };
            allSections = allSectionStrEx.Split(separator);
            return allSections;
        }
  1. 把结果存到二维字典。
 /// <summary>
        /// 遍历INI文件,返回二维字典所有内容
        /// </summary>
        public Dictionary<string, Dictionary<string, string>> TraverseIni()
        {
    
    
            //二维字典
            Dictionary<string, Dictionary<string, string>> ConfPlugInfo = new Dictionary<string, Dictionary<string, string>>();
            //读取配置算子文件中所有算子段落
            string[] sections = GetAllSections();
            int sectionsLength = sections.Length;
            //遍历所有段落
            for (int i = 0; i < sectionsLength; i++)
            {
    
    
                string section = sections[i];
                //获取该段落的所有键
                string[] keys = GetAllKeys(section);
                int keysLength = keys.Length;
                //一维存键值对
                Dictionary<string, string> keyValue = new Dictionary<string, string>();
                //遍历所有的键,提取其值
                for (int j = 0; j < keysLength; j++)
                {
    
    
                    string key = keys[j];
                    string value = ReadString(section, key, "");
                    keyValue.Add(key, value);
                }
                ConfPlugInfo.Add(section, keyValue);
            }
            return ConfPlugInfo;
        }

3.结果展示

在这里插入图片描述

四、总结

C#操作INI文件的开源库有很多IniParser、Nini、SimpleIni等,当然如果时间充裕,能自己折腾一下也是很快乐的。不要求自己造轮子,但希望能掌握造轮子的能力。

注:文章部分函数解析参考网上资料!如有侵权,联系删除!
转载本文需要标明出处!
谷子彭:[email protected]

猜你喜欢

转载自blog.csdn.net/a1062484747/article/details/129829383