读写INI文件

300

在.NET框架中是否有可以读写标准.ini文件的类?

[Section]
<keyname>=<value>
...

Delphi有TIniFile组件,我想知道是否有类似的组件可用于C#?


RemObjects有一个Delphi Prism库,名为ShineOn,提供了一个类似的INI文件类。但是你需要有Delphi Prism才能从源代码编译成.NET,因为目前还没有编译好的程序集可用。http://code.remobjects.com/p/shineon/ - Lex Li
2
遇到了同样的问题,我自己编写了一个解析ini文件的库:https://github.com/rickyah/ini-parser/ 希望能对你有所帮助。 - Ricardo Amores
5
就像 Ricky 一样,我决定自己制作解决方案。它可以在以下网站找到:https://github.com/MarioZ/MadMilkman.Ini - Mario Z
18个回答

298

前言

首先,阅读这篇 MSDN 博客文章关于 INI 文件的限制。如果符合您的需求,请继续阅读。

这是我编写的一个简洁实现,利用了原始的 Windows P/Invoke,因此支持安装了 .NET 的所有 Windows 版本(即 Windows 98 - Windows 11)。我在此将其发布到公共领域 - 您可以在商业上免费使用它,无需归属。

微小的类

向您的项目添加一个名为 IniFile.cs 的新类:

using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;

// Change this to match your program's normal namespace
namespace MyProg
{
    class IniFile   // revision 11
    {
        string Path;
        string EXE = Assembly.GetExecutingAssembly().GetName().Name;

        [DllImport("kernel32", CharSet = CharSet.Unicode)]
        static extern long WritePrivateProfileString(string Section, string Key, string Value, string FilePath);

        [DllImport("kernel32", CharSet = CharSet.Unicode)]
        static extern int GetPrivateProfileString(string Section, string Key, string Default, StringBuilder RetVal, int Size, string FilePath);

        public IniFile(string IniPath = null)
        {
            Path = new FileInfo(IniPath ?? EXE + ".ini").FullName;
        }

        public string Read(string Key, string Section = null)
        {
            var RetVal = new StringBuilder(255);
            GetPrivateProfileString(Section ?? EXE, Key, "", RetVal, 255, Path);
            return RetVal.ToString();
        }

        public void Write(string Key, string Value, string Section = null)
        {
            WritePrivateProfileString(Section ?? EXE, Key, Value, Path);
        }

        public void DeleteKey(string Key, string Section = null)
        {
            Write(Key, null, Section ?? EXE);
        }

        public void DeleteSection(string Section = null)
        {
            Write(null, null, Section ?? EXE);
        }

        public bool KeyExists(string Key, string Section = null)
        {
            return Read(Key, Section).Length > 0;
        }
    }
}

如何使用

有以下三种方式之一打开INI文件:

// Creates or loads an INI file in the same directory as your executable
// named EXE.ini (where EXE is the name of your executable)
var MyIni = new IniFile();

// Or specify a specific name in the current dir
var MyIni = new IniFile("Settings.ini");

// Or specify a specific name in a specific dir
var MyIni = new IniFile(@"C:\Settings.ini");

您可以这样编写一些值:

MyIni.Write("DefaultVolume", "100");
MyIni.Write("HomePage", "http://www.google.com");

创建一个像这样的文件:
[MyProg]
DefaultVolume=100
HomePage=http://www.google.com

从INI文件中读取值:

var DefaultVolume = MyIni.Read("DefaultVolume");
var HomePage = MyIni.Read("HomePage");

可选地,您可以设置[Section]的:

MyIni.Write("DefaultVolume", "100", "Audio");
MyIni.Write("HomePage", "http://www.google.com", "Web");

创建一个像这样的文件:
[Audio]
DefaultVolume=100

[Web]
HomePage=http://www.google.com

你也可以这样检查一个键是否存在:

if(!MyIni.KeyExists("DefaultVolume", "Audio"))
{
    MyIni.Write("DefaultVolume", "100", "Audio");
}

你可以这样删除一个键:
MyIni.DeleteKey("DefaultVolume", "Audio");

您也可以这样删除一个整个部分(包括所有键):

MyIni.DeleteSection("Web");

欢迎随时评论并提出改进意见!


12
我来晚了,但是缺少 GetSections() 方法。 - stil
3
也许更传统的默认方式是针对每个应用程序而非每个程序集使用 .ini 文件,例如 Path.GetFullPath(IniPath ?? Path.ChangeExtension(Application.ExecutablePath, ".ini")) - Eugene Ryabtsev
3
非常棒!放到GitHub上吗? - Emrys Myrooin
15
现在已经老了,尽管我很尊重陈纯洁,但那篇文章中的许多限制都是特定于 Windows 中 INI 库的限制,而不是 INI 格式本身的限制。其他限制,如细粒度权限,可以通过使用多个文件轻松规避。即使在今天,一个正式的现代化 INI 库仍将受到热烈欢迎。 - Joel Coehoorn
2
读取操作无法正常工作,您能否提供另一个简单的示例,只需提供键即可读取值? - Touhid
显示剩余19条评论

213
.NET框架的创建者希望您使用基于XML的配置文件,而不是INI文件。因此,没有内置机制来读取它们。
但是,有第三方解决方案可用。

8
我认为可用的大量功能实际上导致了.NET配置文件变成了一个巨大而充满魔法的怪物,它们已经成为“配置文件中的代码”,这导致了很多复杂性、奇怪的行为,并使配置管理变得更加困难。(我在看你,数据库“提供程序”和连接字符串。) 因此,INI文件通常也更适合非手动编辑。 - jpmc26
1
我喜欢旧方法(P/Invoke),你可以像这样在旧方法中使用Unicode:File.WriteAllBytes(path,new byte[] {0xFF,0xFE}); - sailfish009
2
好的包,但还有改进的空间。它无法完全解析包含 '=' 或 '\n' 的值。 - Ahmad Behjati

67

这篇在CodeProject上的文章 "使用C#处理INI文件的类" 可以帮助您。

作者创建了一个名为 "Ini" 的C#类,它公开了两个来自KERNEL32.dll的函数。这两个函数是: WritePrivateProfileStringGetPrivateProfileString。 您将需要两个命名空间:System.Runtime.InteropServicesSystem.Text

使用Ini类的步骤

在您项目的命名空间定义中添加

using INI;
创建一个像这样的INI文件。
INIFile ini = new INIFile("C:\\test.ini");

使用IniWriteValue方法向特定 section 写入新值,或使用IniReadValue方法从特定 section 的键读取值。

注意:如果您从头开始编写代码,可以阅读此 MSDN 文章如何将应用程序配置文件添加到 C# 项目中。这是一种更好的配置应用程序的方式。


1
我想要读取完整的INI文件。如何做到这一点,而不是只读取部分和键? - venkat
这个对我起初是有效的,但后来从另一个点开始就不行了。仍然不知道底层发生了什么变化。 - nawfal
2
注意不要使用这些已弃用的Win32 API函数。更多信息请参考:http://stackoverflow.com/questions/11451641/writeprivateprofilestring-is-not-adding-proprty-to-the-end - Pedro77
1
我曾经使用过这种方法,但是从Win7开始的安全增强措施基本上让我放弃了这种方法。你仍然可以使用这种方法,但是你必须将.ini文件存储在ProgramData中,并让你的应用程序在那里读取/写入。 - Jess
1
不要将应用程序配置ini文件保存在ProgramData中。 它们不属于注册表或ProgramData。 配置文件应该在LocalApplicationData文件夹中。 - deegee
一个简单的INI文件读写器,可以满足我的需求,而不需要臃肿的库代码。 :) - Naveen Kumar V

49

我找到了这个简单的实现:

http://bytes.com/topic/net/insights/797169-reading-parsing-ini-file-c

对于我所需要的功能来说,它运作得很好。

以下是使用它的方法:

public class TestParser
{
    public static void Main()
    {
        IniParser parser = new IniParser(@"C:\test.ini");

        String newMessage;

        newMessage = parser.GetSetting("appsettings", "msgpart1");
        newMessage += parser.GetSetting("appsettings", "msgpart2");
        newMessage += parser.GetSetting("punctuation", "ex");

        //Returns "Hello World!"
        Console.WriteLine(newMessage);
        Console.ReadLine();
    }
}

以下是代码:

using System;
using System.IO;
using System.Collections;

public class IniParser
{
    private Hashtable keyPairs = new Hashtable();
    private String iniFilePath;

    private struct SectionPair
    {
        public String Section;
        public String Key;
    }

    /// <summary>
    /// Opens the INI file at the given path and enumerates the values in the IniParser.
    /// </summary>
    /// <param name="iniPath">Full path to INI file.</param>
    public IniParser(String iniPath)
    {
        TextReader iniFile = null;
        String strLine = null;
        String currentRoot = null;
        String[] keyPair = null;

        iniFilePath = iniPath;

        if (File.Exists(iniPath))
        {
            try
            {
                iniFile = new StreamReader(iniPath);

                strLine = iniFile.ReadLine();

                while (strLine != null)
                {
                    strLine = strLine.Trim().ToUpper();

                    if (strLine != "")
                    {
                        if (strLine.StartsWith("[") && strLine.EndsWith("]"))
                        {
                            currentRoot = strLine.Substring(1, strLine.Length - 2);
                        }
                        else
                        {
                            keyPair = strLine.Split(new char[] { '=' }, 2);

                            SectionPair sectionPair;
                            String value = null;

                            if (currentRoot == null)
                                currentRoot = "ROOT";

                            sectionPair.Section = currentRoot;
                            sectionPair.Key = keyPair[0];

                            if (keyPair.Length > 1)
                                value = keyPair[1];

                            keyPairs.Add(sectionPair, value);
                        }
                    }

                    strLine = iniFile.ReadLine();
                }

            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                if (iniFile != null)
                    iniFile.Close();
            }
        }
        else
            throw new FileNotFoundException("Unable to locate " + iniPath);

    }

    /// <summary>
    /// Returns the value for the given section, key pair.
    /// </summary>
    /// <param name="sectionName">Section name.</param>
    /// <param name="settingName">Key name.</param>
    public String GetSetting(String sectionName, String settingName)
    {
        SectionPair sectionPair;
        sectionPair.Section = sectionName.ToUpper();
        sectionPair.Key = settingName.ToUpper();

        return (String)keyPairs[sectionPair];
    }

    /// <summary>
    /// Enumerates all lines for given section.
    /// </summary>
    /// <param name="sectionName">Section to enum.</param>
    public String[] EnumSection(String sectionName)
    {
        ArrayList tmpArray = new ArrayList();

        foreach (SectionPair pair in keyPairs.Keys)
        {
            if (pair.Section == sectionName.ToUpper())
                tmpArray.Add(pair.Key);
        }

        return (String[])tmpArray.ToArray(typeof(String));
    }

    /// <summary>
    /// Adds or replaces a setting to the table to be saved.
    /// </summary>
    /// <param name="sectionName">Section to add under.</param>
    /// <param name="settingName">Key name to add.</param>
    /// <param name="settingValue">Value of key.</param>
    public void AddSetting(String sectionName, String settingName, String settingValue)
    {
        SectionPair sectionPair;
        sectionPair.Section = sectionName.ToUpper();
        sectionPair.Key = settingName.ToUpper();

        if (keyPairs.ContainsKey(sectionPair))
            keyPairs.Remove(sectionPair);

        keyPairs.Add(sectionPair, settingValue);
    }

    /// <summary>
    /// Adds or replaces a setting to the table to be saved with a null value.
    /// </summary>
    /// <param name="sectionName">Section to add under.</param>
    /// <param name="settingName">Key name to add.</param>
    public void AddSetting(String sectionName, String settingName)
    {
        AddSetting(sectionName, settingName, null);
    }

    /// <summary>
    /// Remove a setting.
    /// </summary>
    /// <param name="sectionName">Section to add under.</param>
    /// <param name="settingName">Key name to add.</param>
    public void DeleteSetting(String sectionName, String settingName)
    {
        SectionPair sectionPair;
        sectionPair.Section = sectionName.ToUpper();
        sectionPair.Key = settingName.ToUpper();

        if (keyPairs.ContainsKey(sectionPair))
            keyPairs.Remove(sectionPair);
    }

    /// <summary>
    /// Save settings to new file.
    /// </summary>
    /// <param name="newFilePath">New file path.</param>
    public void SaveSettings(String newFilePath)
    {
        ArrayList sections = new ArrayList();
        String tmpValue = "";
        String strToSave = "";

        foreach (SectionPair sectionPair in keyPairs.Keys)
        {
            if (!sections.Contains(sectionPair.Section))
                sections.Add(sectionPair.Section);
        }

        foreach (String section in sections)
        {
            strToSave += ("[" + section + "]\r\n");

            foreach (SectionPair sectionPair in keyPairs.Keys)
            {
                if (sectionPair.Section == section)
                {
                    tmpValue = (String)keyPairs[sectionPair];

                    if (tmpValue != null)
                        tmpValue = "=" + tmpValue;

                    strToSave += (sectionPair.Key + tmpValue + "\r\n");
                }
            }

            strToSave += "\r\n";
        }

        try
        {
            TextWriter tw = new StreamWriter(newFilePath);
            tw.Write(strToSave);
            tw.Close();
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }

    /// <summary>
    /// Save settings back to ini file.
    /// </summary>
    public void SaveSettings()
    {
        SaveSettings(iniFilePath);
    }
}

26

joerage的回答中的代码很启发人。

不幸的是,它改变了键的字符大小写,并且不能处理注释。因此,我编写了一个更加强大的代码,能够读取(仅限)非常混乱的INI文件,并允许按原样检索键。

它使用一些LINQ,一个嵌套的不区分大小写的字符串字典来存储部分、键和值,并在一次读取文件时进行读取。

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

class IniReader
{
    Dictionary<string, Dictionary<string, string>> ini = new Dictionary<string, Dictionary<string, string>>(StringComparer.InvariantCultureIgnoreCase);

    public IniReader(string file)
    {
        var txt = File.ReadAllText(file);

        Dictionary<string, string> currentSection = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);

        ini[""] = currentSection;

        foreach(var line in txt.Split(new[]{"\n"}, StringSplitOptions.RemoveEmptyEntries)
                               .Where(t => !string.IsNullOrWhiteSpace(t))
                               .Select(t => t.Trim()))
        {
            if (line.StartsWith(";"))
                continue;

            if (line.StartsWith("[") && line.EndsWith("]"))
            {
                currentSection = new Dictionary<string, string>(StringComparer.InvariantCultureIgnoreCase);
                ini[line.Substring(1, line.LastIndexOf("]") - 1)] = currentSection;
                continue;
            }

            var idx = line.IndexOf("=");
            if (idx == -1)
                currentSection[line] = "";
            else
                currentSection[line.Substring(0, idx)] = line.Substring(idx + 1);
        }
    }

    public string GetValue(string key)
    {
        return GetValue(key, "", "");
    }

    public string GetValue(string key, string section)
    {
        return GetValue(key, section, "");
    }

    public string GetValue(string key, string section, string @default)
    {
        if (!ini.ContainsKey(section))
            return @default;

        if (!ini[section].ContainsKey(key))
            return @default;

        return ini[section][key];
    }

    public string[] GetKeys(string section)
    {
        if (!ini.ContainsKey(section))
            return new string[0];

        return ini[section].Keys.ToArray();
    }

    public string[] GetSections()
    {
        return ini.Keys.Where(t => t != "").ToArray();
    }
}

5
谢谢您没有把那个 catch (Exception ex) { throw ex; } 放进去。 - Mark Schultheiss
2
很好!至少需要一些更改才能更好地工作。第16行:ini[""] = currentSection; 更改为://ini[""] = currentSection;这必须被删除,因为每次第一个元素[0]都将是一个空段,由于此初始化。第36行:currentSection[line.Substring(0, idx)] = line.Substring(idx + 1); 更改为: currentSection[line.Substring(0, idx).Trim()] = line.Substring(idx + 1).Trim();键和值应该独立修剪,而不仅仅是在行修剪上。在INI等配置文件中,通常会将K->V对齐在节内。谢谢! - LXSoft
我们已经有很长一段时间没有联系了。非常感谢您的建议。它们都很有道理,值得对这段代码进行良好的更新。 - Larry

16
我想介绍一个IniParser库,完全使用c#创建而没有任何OS的依赖,这使它与Mono兼容。采用MIT许可证开源-所以可以在任何代码中使用。
您可以检查GitHub上的源代码,也可以作为NuGet软件包使用。
它可以进行强大的配置,并且非常简单易用
对不起,打扰一下,但我希望它能对重新访问此答案的任何人有所帮助。

12

如果您只需要读取访问而不是写入访问,并且正在使用Microsoft.Extensions.Confiuration(默认情况下与ASP.NET Core捆绑在一起,但也适用于常规程序),则可以使用NuGet包Microsoft.Extensions.Configuration.Ini将ini文件导入到配置设置中。

public Startup(IHostingEnvironment env)
{
    var builder = new ConfigurationBuilder()
        .SetBasePath(env.ContentRootPath)
        .AddIniFile("SomeConfig.ini", optional: false);
    Configuration = builder.Build();
}

1
只是补充一下,你可以使用 Configuration["keyname"] 获取键。 - kofifus
@scott 我遇到的问题是 IIS 在应用程序运行时出现了无法识别的情况。虽然已经部署好了,也在那里,但却没有被使用。HTTP 500.30 错误返回,并且 IIS 应用程序日志显示“找不到配置文件且不可选”。 - one.beat.consumer

5

PeanutButter.INI是一个用于操作INI文件的Nuget打包类。它支持读/写,包括注释——在写入时保留您的注释。它似乎相当受欢迎,经过测试并且易于使用。它也是完全免费和开源的。

声明:我是PeanutButter.INI的作者。


请问您能提供PeanutButter.INI文档的链接吗? - Shroombaker
1
请查看以下链接:https://github.com/fluffynuts/PeanutButter/blob/master/source/INI/PeanutButter.INI.Tests/TestINI.cs - daf

4

如果你只想要一个简单的阅读器,没有章节和其他dll,这里有一个简单的解决方案:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tool
{
    public class Config
    {
        Dictionary <string, string> values;
        public Config (string path)
        {
            values = File.ReadLines(path)
            .Where(line => (!String.IsNullOrWhiteSpace(line) && !line.StartsWith("#")))
            .Select(line => line.Split(new char[] { '=' }, 2, 0))
            .ToDictionary(parts => parts[0].Trim(), parts => parts.Length>1?parts[1].Trim():null);
        }
        public string Value (string name, string value=null)
        {
            if (values!=null && values.ContainsKey(name))
            {
                return values[name];
            }
            return value;
        }
    }
}

使用示例:

    file = new Tool.Config (Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + "\\config.ini");
    command = file.Value ("command");
    action = file.Value ("action");
    string value;
    //second parameter is default value if no key found with this name
    value = file.Value("debug","true");
    this.debug = (value.ToLower()=="true" || value== "1");
    value = file.Value("plain", "false");
    this.plain = (value.ToLower() == "true" || value == "1");

同时,配置文件内容(如您所见)支持 # 符号作为行注释:

#command to run
command = php

#default script
action = index.php

#debug mode
#debug = true

#plain text mode
#plain = false

#icon = favico.ico

4
通常情况下,使用C#和.NET框架创建应用程序时,不会使用INI文件。更常见的是将设置存储在基于XML的配置文件或注册表中。 但是,如果您的软件需要与旧版本应用程序共享设置,则使用其配置文件可能比在其他地方重复信息更容易。
.NET框架不直接支持使用INI文件。但是,您可以使用平台调用服务(P / Invoke)和Windows API函数来读取和写入文件。在此链接中,我们创建了一个表示INI文件并使用Windows API函数进行操作的类。 请查看以下链接。 读写INI文件

5
不要进入注册表! 应用程序配置数据不应该保存在注册表中。 - deegee
@deegee:有趣的是,微软表示INI文件已被弃用,推荐使用注册表 - Thomas Weller
1
@ThomasWeller - 我不是来和人争论的。除非绝对必要,否则不应使用注册表。即使如此,也应该仅保留供Microsoft Windows使用。几乎没有第三方软件在卸载其软件时清理其注册表键,导致注册表处于混乱状态,他们本来就应该远离它。 INI文件、XML文件、JSON文件和其他专为此功能设计的配置文件格式应放置在AppData文件夹中,这是它们应该放置的地方。 - deegee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接