一、INI文件介绍
1.背景
INI文件是一种配置文件格式,是Initialization file的缩写,即为初始化文件,通
常用于Windows操作系统中的应用程序中。虽然INI是远古时期的产物,但它
依然能屹立在这高手纵云(XML、YAML、JSON…)的年代。
2.格式
INI结构非常简单文件是一种易于编辑和阅读的文本,由节(Section)、键(key)、值(value)、注释(comments)组成的两层结构。键值对用”=“隔开,左边为键,右边为值,每个键值对都要归纳于段落中,段落必须写在”[]“里。注释则以分号”;“开头即可,如下:
[参数1]
;记录参数1的键1值
键1=值1
键2=值2
键3=值3
[参数2]
键1=值1
键2=值2
键3=值3
3.后缀(文件扩展名)
.ini、.cfg、.conf、.txt
4.特点:
优点:
- 结构简单,两层结构一目了然,符合人类理解事物的逻辑,易于维护;
- 读写速度快,适合作为软件系统各模块的配置表,减少软件启动时间;
- 接口简单,上手容易,对新手友好。
缺点:
- 结构过于简单,只有两层结构,不适合描绘复杂类型数据、多级数据;
- 大小限制64kb;
- 操作不当,中文描绘数据变乱码;
- 只支持字符串类型;
二、增删改
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,
- 使用getPrivateProfileSectionNames 函数 检索初始化文件中所有节的名称。
- 使用getPrivateProfileSection获取节下面的所有键和值。
我使用了另一种方法:
- 使用GetPrivateProfileString(null, null, “”, allSectionByte, 4096, this.Filepath)方式获取文件中所有节点。
- 需要传入一个byte[]数组用于接受遍历结果叠加的二进制,使用Encoding.GetEncoding()或者ASCIIEncoding类下的GetString()方法转成字符串后,每个节点之间默认使用‘\0’作为分隔符,如果我们直接使用或者打印,只能得到第一各节点,因为“\0”是结束符。
- 使用String.Replace()函数把”\0“替换成"|"。
- 使用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;
}
- 同理可使用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;
}
- 把结果存到二维字典。
/// <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等,当然如果时间充裕,能自己折腾一下也是很快乐的。不要求自己造轮子,但希望能掌握造轮子的能力。
注:文章部分函数解析参考网上资料!如有侵权,联系删除!
转载本文需要标明出处!
谷子彭:1062484747@163.com