65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 INI 文件存储应用程序设置的跨平台 C# 类 - 第二版

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (14投票s)

2013年9月1日

CPOL

9分钟阅读

viewsIcon

79640

downloadIcon

4073

该类在Mono或.NET下运行,允许使用Windows风格的“INI”文件来存储和检索应用程序设置。

介绍  

几年前,在我的一篇上一篇文章中,我介绍了一个非常基础且粗糙的版本,用于在.NET和Mono下管理INI文件的类:INIFile。其思想是使用Dictionary来保存文件中找到的所有键/值对(缓存),当然,直接解析INI,而不使用任何Windows API。

这种方法的缺点是,每次将缓存的内容写回磁盘时,INI文件都会被覆盖。这会擦除任何被注释掉的行或用户可能想要保留的任何其他自定义内容,而不仅仅是有效的键/值对。

写完文章后,我花了一些时间来完善这个类,添加了必要的函数来保留原始文件内容,包括注释等,然后编写了一个自定义的ApplicationSettings类,该类利用INIFile来持久化应用程序设置。在过去几年里,这两个类是我所有项目中存储应用程序设置的标准方式,我对它们的工作方式非常满意。:)

这篇文章是由于最近一篇关于旧文章的论坛帖子而产生的,这提醒我我从未更新过它。

这篇文章只是对旧文章的重新编辑,所以大部分内容是相同的。如果您熟悉旧文章,您可以直接下载新版本,并查看“使用代码”段落 - 我对其进行了一些更改,它还展示了一个新功能:自动刷新。

那么,就此开始吧 - 希望您喜欢!:)

什么是INI文件? 

 “INI”文件在整个Windows操作系统中被广泛使用。在注册表出现之前它们就存在了,并且至今仍然存在。    

它们是文本文件,通常(但不一定)以“.ini”扩展名结尾,用于存储各种设置。

它们的结构相当简单:设置被组织在“节”中,节标题用方括号(“[” “]”)括起来。每个节包含一个或多个“键/值”字符串对。每个键在一个节内是唯一的,并标识一个特定的设置。

INI文件内容的一个例子是
[Section1]
Key1=somevalue
Key2=somevalue
...
[Section2]
Key1=somevalue
Key10=somevalue
...
使用INI文件可以方便地存储您的应用程序设置,因为文件格式易于阅读、理解,并且可以通过文本编辑器进行修改。 

背景 

在Windows应用程序中使用INI文件通常是通过“kernel32.dll”库公开的一些API调用来实现的。在C#中,这可以通过使用“DllImport”指令来完成。

#region "WIN32API"
 
    [DllImport("kernel32.dll")] private static extern int GetPrivateProfileInt
	([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
	[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,int nDefault,
	[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
    [DllImport("kernel32.dll")] private static extern int GetPrivateProfileString
	([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
	[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
	[MarshalAs(UnmanagedType.LPStr)] string lpDefault,
	byte[] lpReturnedString,int nSize,[MarshalAs(UnmanagedType.LPStr)] 
	string lpFileName);
    [DllImport("kernel32.dll")] private static extern int GetPrivateProfileString
	([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
	[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
	[MarshalAs(UnmanagedType.LPStr)] string lpDefault,
	[MarshalAs(UnmanagedType.LPStr)] string lpReturnedString,
	int nSize,[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
    [DllImport("kernel32.dll")] private static extern int WritePrivateProfileString
	([MarshalAs(UnmanagedType.LPStr)] string lpApplicationName,
	[MarshalAs(UnmanagedType.LPStr)] string lpKeyName,
	[MarshalAs(UnmanagedType.LPStr)] string lpString,
	[MarshalAs(UnmanagedType.LPStr)] string lpFileName);
 
#endregion  

我决定编写自己的跨平台类,以便在Mono和.NET下轻松使用INI文件。结果就是INIFile类,它与标准的Windows INI文件兼容,但有几点例外:   

  1. 节名和键名区分大小写。
  2. 它使用缓存来提高性能。

缓存是通过Dictionary实现的,它将所有节和键/值对保存在内存中。

Using the Code

INI文件在创建类的实例时被读取和解析,其内容被存储在缓存中。INI文件名(目标文件的完整路径)被传递给构造函数。

INIFile MyINIFile = new INIFile("myinifilename.ini"); 
用户程序可以通过调用GetValue()方法,提供节名和键名,来查询缓存的内容。
string Value = MyINIFile.GetValue("Section", "Key", "Default value");
为了与Windows API调用的行为保持兼容,如果INI文件不存在或请求了一个不存在的节或键,则不会抛出异常。相反,将返回用户提供的默认值。这允许轻松处理设置被最终用户可选修改而默认值被硬编码在软件中的情况。 

还有一个延迟加载INI文件内容的选项(只需将true作为第二个参数传递给构造函数调用),这告诉类实例在您第一次读取或修改值时加载INI文件内容。

INIFile MyINIFile = new INIFile("myinifilename.ini", true); // Activate lazy loading 
虽然大多数情况下您不需要延迟加载,但在某些特定场景下它可能很有用,例如您需要创建INIFile对象并延迟文件的实际解析,无论是出于性能还是行为原因。

可以通过调用SetValue()方法来更改值。

MyINIFile.SetValue("Section", "Key", "Value");
如果提供的节或键不存在,它们将被自动添加到缓存中。所有更改保存回INI文件是通过显式调用Flush()方法完成的。
MyINIFile.Flush();

如果缓存中有任何修改,所有节和键/值对将被写回磁盘,覆盖旧文件。

如果文件不存在,它将被创建。用户程序有责任通过显式避免两个不同的INIFile类实例指向同一文件的情况来确保文件内容的完整性。

您还可以向构造函数提供一个值为true的第三个Boolean参数,以激活自动刷新

INIFile MyINIFile = new INIFile("myinifilename.ini", false, true); // Example: activate automatic flushing, but not lazy loading

自动刷新意味着每次使用SetValue()修改值时,缓存的内容都会写回磁盘,覆盖原始文件。这意味着您在完成修改后不必显式调用Flush()方法。

如果您使用自动刷新并连续修改一系列值,性能会显著下降。在这种情况下,我强烈建议关闭自动刷新,并调用Flush()

如果您担心由于Exception而丢失已做的修改,应该使用try...finally构造:  

try
{
	MyINIFile.SetValue("Section1", "Key1", Value1);
	MyINIFile.SetValue("Section1", "Key2", Value2);
	
	// ... other code which may throw an exception

	MyINIFile.SetValue("Section2", "Key3", Value3);
}
finally
{
	MyINIFile.Flush();
} 
可以通过调用Refresh()方法强制刷新缓存。INI文件将再次被解析,缓存的内容将被文件内容替换。在最后一次调用Flush()(或自动刷新开启时调用SetValue())之后对缓存所做的任何修改都将丢失。
MyINIFile.Refresh();  
在大多数典型情况下,管理INI文件的最佳方法是每次需要读取或写入应用程序设置时都创建一个INIFile类的实例,然后销毁它。这将确保任何通过文本编辑器进行的更改不会被覆盖。

例如,您可以在应用程序启动时创建一个类的实例来读取并存储设置,然后在应用程序关闭时,或当用户在您的“更改设置”对话框中按下“确定”按钮时,创建另一个实例来写入对设置的更改,依此类推。

由于所有值都是string,用户程序可以轻松地将任何期望的数据类型编码为string,以便将其写入INI文件,并在读取相同文件时相应地解码。然而,INIFile类提供了一些getter和setter来自动管理一些基本数据类型,以满足所有典型需求。

internal string GetValue(string SectionName, string Key, string DefaultValue)
internal bool GetValue(string SectionName, string Key, bool DefaultValue)
internal int GetValue(string SectionName, string Key, int DefaultValue)
internal double GetValue(string SectionName, string Key, double DefaultValue)
internal byte[] GetValue(string SectionName, string Key, byte[] DefaultValue)
 
internal void SetValue(string SectionName, string Key, string Value)
internal void SetValue(string SectionName, string Key, bool Value)
internal void SetValue(string SectionName, string Key, int Value)
internal void SetValue(string SectionName, string Key, double Value)
internal void SetValue(string SectionName, string Key, byte[] Value)
intdouble简单地转换为string(使用中性区域设置,因此文件将跨区域设置兼容),而bool将转换为整数值,其中0表示false,任何其他值表示true。这使得用户可以通过文本编辑器轻松修改这些值。

还可以存储byte数组。当存储哈希值、UUID或任何其他类型的二进制数据时,这非常有用。框架提供了将string和其他数据与字节数组相互转换的方法,因此使用此功能非常简单。字节数组在写入INI文件时被转换为十六进制string,这使得可以通过文本编辑器修改它们的值。

关于线程安全和跨进程访问的说明 

该类包含一个基本的锁定系统以实现线程安全。所有对类方法的调用都是线程安全的。

锁定是针对文件和缓存的访问进行的,并且为了保证多线程应用程序中最大程度的数据完整性,两者使用了相同的锁。

也就是说,您必须记住,用户代码有责任确保完整的数据完整性,因为锁定是为每个方法调用单独进行的!例如,两个不同的线程对同一类的实例执行几次SetValue()然后Flush()的调用将需要用户进行显式锁定,以防止数据丢失。如果线程使用该类的不同实例指向同一个文件,那么使用锁定来防止数据丢失以及文件访问时的异常就更加重要了。

同样的情况也适用于从不同进程访问文件:您必须要么设置一个进程间锁定系统,要么确保管理所有可能的并发问题。

源代码和示例代码

您可以通过此页面顶部的链接下载该类的源代码以及一些示例代码。 

它是在Mono 2.6.7中使用MonoDevelop 2.4开发的,但与所有其他从2.0开始的Mono版本以及.NET完全兼容 - 只需用Visual Studio打开解决方案,将代码复制粘贴到新的Visual Studio项目中,或者直接用.NET编译器编译源代码。

这是示例代码的摘录,展示了如何使用该类的功能

StreamReader sr = null;
try	
{
	Console.WriteLine("Creating INIFile object for \"test.ini\"...");
 
	INIFile MyINIFile = new INIFile("test.ini");
 
	Console.WriteLine("\nGetting values...\n");
 
	int Value1 = MyINIFile.GetValue("Section1","Value1",0);
	bool Value2 = MyINIFile.GetValue("Section1","Value2",false);
	double Value3 = MyINIFile.GetValue("Section1","Value3",(double)0);
	byte[] Value4 = MyINIFile.GetValue("Section1","Value4",(byte[])null);
 
	Console.Write("(int) Value1=");
	Console.WriteLine(Value1.ToString());
 
	Console.Write("(bool) Value2=");
	Console.WriteLine(Value2.ToString());
 
	Console.Write("(double) Value3=");
	Console.WriteLine(Value3.ToString());
 
	Console.Write("(byte[]) Value4=");
	Console.WriteLine(PrintByteArray(Value4));
 
	Console.WriteLine("\nSetting values...\n");
 
	Value1++;
	Value2 = !Value2;
	Value3 += 0.75;
	Value4 = new byte[] { 10, 20, 30, 40 };
	
	MyINIFile.SetValue("Section1","Value1", Value1);
	MyINIFile.SetValue("Section1","Value2", Value2);
	MyINIFile.SetValue("Section1","Value3", Value3);
	MyINIFile.SetValue("Section1","Value4", Value4);
 
	Console.Write("(int) Value1=");
	Console.WriteLine(Value1.ToString());
 
	Console.Write("(bool) Value2=");
	Console.WriteLine(Value2.ToString());
 
	Console.Write("(double) Value3=");
	Console.WriteLine(Value3.ToString());
 
	Console.Write("(byte[]) Value4=");
	Console.WriteLine(PrintByteArray(Value4));
 
	Console.WriteLine("\nFlushing cache...");
 
	MyINIFile.Flush();
	
	Console.WriteLine("\nFile content:\n");
	
	sr = new StreamReader("test.ini");
	string s;
	while ((s=sr.ReadLine()) != null) Console.WriteLine(s);
	
	Console.WriteLine("\nDone.");
}
catch (Exception ex)
{
	Console.Write("\n\nEXCEPTION: ");
	Console.WriteLine(ex.Message);
}
finally
{
	if (sr != null) sr.Close();
	sr = null;
}
这是首次运行示例的结果打印输出
Creating INIFile object for "test.ini"...
 
Getting values...
 
(int) Value1=0
(bool) Value2=False
(double) Value3=0
(byte[]) Value4=
 
Setting values...
 
(int) Value1=1
(bool) Value2=True
(double) Value3=0,75
(byte[]) Value4=10, 20, 30, 40
 
Flushing cache...
 
File content:
 
[Section1]
Value1=1
Value2=1
Value3=0.75
Value4=0a141e28
 
Done. 

现在,您应该会在可执行文件所在的文件夹(INIFileTest/bin/Release 或 INIFileTest/bin/Debug)中看到“test.ini”文件。

现在,用gedit、kate、vi、joe等文本编辑器编辑它,并更改其内容,例如这样

[MySection]
;This section is not used
;I like to use a ";" to start comments, but you can just write anything you like as long as it
;doesn't match a key/value pair you use in your application
 
[Section1]
Value1=3
Value2=0
 
;Here I'm commenting out and old value for "Value3" and inserting a new one to my liking
;Value3=2.25
Value3=12.34
 
Value4=0a141e28

再次运行可执行文件,您应该会得到这个结果

Creating INIFile object for "test.ini"...
 
Getting values...
 
(int) Value1=3
(bool) Value2=False
(double) Value3=12,34
(byte[]) Value4=10, 20, 30, 40
 
Setting values...
 
(int) Value1=4
(bool) Value2=True
(double) Value3=13,09
(byte[]) Value4=10, 20, 30, 40
 
Flushing cache...
 
File content:
 
[MySection]
;This section is not used
;I like to use a ";" to start comments, but you can just write anything you like as long as it
;doesn't match a key/value pair you use in your application
 
[Section1]
Value1=4
Value2=1
 
;Here I'm commenting out and old value for "Value3" and inserting a new one to my liking
;Value3=2.25
Value3=13.09
 
Value4=0a141e28
 

Done. 

如您所见,注释和所有自定义用户内容都被保留了。

值得关注的点  

该类可以从很多方面进行改进:实现更好的缓存系统,在处理时自动刷新实现IDisposable接口(尽管我个人不喜欢这个解决方案,所以我放弃了这个想法),使用泛型和包装类自动编码任何类型数据的方法,甚至更好的是,使用反射将类自动转储和重新加载到INI文件中,将它们映射到节/键结构,等等。

基础知识都已具备,并且很容易为该类添加更多功能。尽情享受吧!:)

历史


  • 2013-09-01 - V2.1 - 发布新版本,该版本保留文件内容并提供自动刷新
  • 2009-04-13 - V1.0 - 首次发布   
© . All rights reserved.