如何在Windows Forms应用程序中保存应用程序设置?

652
我想实现的很简单:我有一个使用路径读取信息的Windows Forms (.NET 3.5)应用程序。用户可以使用我提供的选项表单修改此路径。
现在,我希望将此路径值保存到文件中以备后用。这将是保存到该文件的许多设置之一。该文件将直接位于应用程序文件夹中。
我了解到有三个选择:
ConfigurationSettings文件(appname.exe.config)     注册表     自定义XML文件
据我所知,.NET配置文件不适用于将值保存回去。至于注册表,我希望尽可能远离它。
这是否意味着我应该使用自定义XML文件来保存配置设置?
如果是这样,我想看到C#代码示例。
我看过其他关于此主题的讨论,但对我来说仍不清楚。

1
这是一个.NET WinForms应用程序吗?如果是的话,您正在开发哪个版本的.NET? - Portman
2
是的,它是一个 .NET Framework 版本为 3.5 的 WinForms 应用程序。 - Fueled
1
你需要保存密码或机密值吗?也许需要进行加密。 - Kiquenet
14个回答

662
如果您使用Visual Studio,获取可持久化设置就很容易。在解决方案资源管理器中右键单击项目,然后选择属性。选择设置选项卡,如果没有设置,则点击超链接。
使用设置选项卡创建应用程序设置。Visual Studio会创建包含继承自ApplicationSettingsBase的单例类SettingsSettings.settingsSettings.Designer.settings文件。您可以从代码中访问此类来读写应用程序设置:
Properties.Settings.Default["SomeProperty"] = "Some Value";
Properties.Settings.Default.Save(); // Saves settings in application configuration file

这种技术适用于控制台、Windows Forms以及其他类型的项目。

请注意需要设置您的设置文件的作用域属性。如果选择应用程序作用域,则Settings.Default.<your property>将是只读的。

参考文献:使用C#在运行时编写用户设置的方法 - 微软文档


2
如果我有一个解决方案,这将适用于整个解决方案还是每个项目? - franko_camron
9
@Four: 我这里有一个.NET 4.0 WinApp项目,我的SomeProperty不是只读的。Settings.Default.SomeProperty = 'value'; Settings.Default.Save(); 运行得很好。这是因为我有用户设置吗? - doekman
4
@Four: 当我把设置从用户级别改为应用程序级别并保存文件时,我发现在生成的代码中setter消失了。这种情况也会发生在客户端框架4.0中... - doekman
3
@Four:非常好的链接,不过你说 Settings.Default.Save() 不起作用是不正确的。正如 @aku 在回答中所说,应用程序范围的设置为只读:对它们进行保存是无效的。使用那个自定义的 PortableSettingsProvider 将用户范围的设置保存到 app.config 中,该文件位于 exe 所在的位置,而不是用户的 AppData 文件夹中。虽然并不是一般情况下的最佳实践,但我在开发过程中使用它来实现在每次编译时使用相同的设置(如果没有它,则每次编译都会产生新的唯一用户文件夹)。 - minnow
8
目前来看,使用.NET 3.5时,似乎可以直接使用Settings.Default.SomeProperty来赋值并进行强类型转换。另外,为了节省其他人的时间(我花了一段时间才弄清楚),你需要输入Properties.Settings.Default或在文件顶部添加using YourProjectNameSpace.Settings,因为单独的“Settings”未被定义或找到。 - eselk
显示剩余10条评论

104

如果您计划在与可执行文件相同的目录中保存文件,这里有一个使用JSON格式的好方案:

using System;
using System.IO;
using System.Web.Script.Serialization;

namespace MiscConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            MySettings settings = MySettings.Load();
            Console.WriteLine("Current value of 'myInteger': " + settings.myInteger);
            Console.WriteLine("Incrementing 'myInteger'...");
            settings.myInteger++;
            Console.WriteLine("Saving settings...");
            settings.Save();
            Console.WriteLine("Done.");
            Console.ReadKey();
        }

        class MySettings : AppSettings<MySettings>
        {
            public string myString = "Hello World";
            public int myInteger = 1;
        }
    }

    public class AppSettings<T> where T : new()
    {
        private const string DEFAULT_FILENAME = "settings.json";

        public void Save(string fileName = DEFAULT_FILENAME)
        {
            File.WriteAllText(fileName, (new JavaScriptSerializer()).Serialize(this));
        }

        public static void Save(T pSettings, string fileName = DEFAULT_FILENAME)
        {
            File.WriteAllText(fileName, (new JavaScriptSerializer()).Serialize(pSettings));
        }

        public static T Load(string fileName = DEFAULT_FILENAME)
        {
            T t = new T();
            if(File.Exists(fileName))
                t = (new JavaScriptSerializer()).Deserialize<T>(File.ReadAllText(fileName));
            return t;
        }
    }
}

1
不需要更改DEFAULT_FILENAME,只需调用settings.Save(theFileToSaveTo)即可;由于全大写,DEFAULT_FILENAME应该是一个常量。如果你想要一个读写属性,请创建一个并让构造函数将其设置为DEFAULT_FILENAME。然后将默认参数值设为null,测试它并使用你的属性作为默认值。这样会多打一些字,但可以给你提供一个更标准的接口。 - Jesse Chisholm
11
如果您还没有引用System.Web.Extensions.dll,则需要引用它。 - TEK
11
我基于这个答案创建了一个完整的库,并进行了许多改进,现在已经发布在nuget上了:https://github.com/Nucs/JsonSettings - NucS
2
除非您正在开发一个只会安装到应用程序数据文件夹的应用程序,否则请勿将文件写入与您的exe相同的文件夹。如果您的exe被安装到程序文件夹中,则用户极有可能无法在该位置保存文件。为什么呢?因为允许应用程序将文件保存到dll和exe所在的位置是一种安全漏洞。 - Andy
1
Trevor,这只是一个非常简单可靠的解决方案!我能够立即保存我的复杂对象...如果使用XML,我需要花费几天时间才能使其正常工作。再次感谢您! - yo3hcv
显示剩余3条评论

71

注册表不允许。您不确定使用您的应用程序的用户是否具有足够的权限可以写入注册表。

您可以使用app.config文件保存应用程序级别的设置(对于每个使用您的应用程序的用户相同)。

我会将特定于用户的设置存储在XML文件中,并将其保存在隔离存储SpecialFolder.ApplicationData目录中。

此外,从.NET 2.0开始,可以将值存储回app.config文件。


9
如果您想要每个登录/用户的设置,可以使用注册表。 - user46915
11
可以使用Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)来获取应用程序数据文件夹路径。 - Kenny Mann
4
注册表不是必需的,也不应该用于存储应用程序设置 - 永远不要这样做。System.Environment.SpecialFolder.LocalApplicationData 是每个用户的本地文件夹。.ApplicationData 是每个用户的漫游文件夹。请参阅http://msdn.microsoft.com/en-us/library/system.environment.specialfolder.aspx - deegee
6
用户注册表可以被写入(许多程序在那里写入信息,用户权限从未是问题)。使用注册表的优点是,如果您有多个应用程序共享同一个文件夹(例如安装程序和应用程序),它们不会共享相同的设置。 - Kesty
5
注册表的主要缺点是难以将设置导出/复制到其他计算机。但我不同意“您不确定使用您的应用程序的用户是否具有足够的权限写入注册表”的说法 - 在HKEY_CURRENT_USER中,您始终拥有写入权限。这可能会被拒绝,但文件系统也可能对当前用户不可访问(所有可能的临时文件夹等)。 - i486
显示剩余4条评论

21

ApplicationSettings 类不支持将设置保存到 app.config 文件中。这是有意设计的;以适当安全的用户帐户运行的应用程序(如 Vista UAC)无法写入程序的安装文件夹。

你可以使用 ConfigurationManager 类来绕过系统限制。但是,一个简单的解决方法是进入“设置”设计器并将设置范围更改为“用户”。如果这造成了困难(例如,该设置与每个用户相关),则应将您的选项功能放在一个单独的程序中,这样您就可以请求提升权限提示。或者放弃使用设置。


请问您能否详细说明一下您上一句话的意思?是要求提升权限来编写app.config文件,还是编写一个单独的应用程序,遍历所有用户的主目录,查找user.config文件并进行编辑? - CannibalSmith
2
单独的程序需要一个清单来请求提升权限。请搜索“asinvoker requireadministrator”以找到正确的语法。编辑user.config既不切实际,也不必要。 - Hans Passant

20

我想分享我建立的一个库。这是一个小型的库,但在我看来比.settings文件有很大的改进。

这个库叫做Jot (GitHub)。这里是我以前写的一个Code Project文章

这是如何使用它来跟踪窗口的大小和位置:

public MainWindow()
{
    InitializeComponent();

    _stateTracker.Configure(this)
        .IdentifyAs("MyMainWindow")
        .AddProperties(nameof(Height), nameof(Width), nameof(Left), nameof(Top), nameof(WindowState))
        .RegisterPersistTrigger(nameof(Closed))
        .Apply();
}

与 .settings 文件相比的优点:代码量要少得多,而且由于您只需要提及每个属性 一次,因此出错的可能性要小得多。

使用设置文件时,您需要在代码中提及每个属性五次:一次是当您明确创建该属性时,另外四次是在复制值的代码中。

存储、序列化等完全可配置。当目标对象由一个 IoC 容器创建时,您可以[连接它][], 以便自动应用跟踪到解析的所有对象,这样您所需做的就是在属性上贴上 [Trackable] 属性使其持久。

它可以高度配置,您可以配置: - 数据何时全局持久化和应用或针对每个跟踪的对象 - 序列化方式 - 存储位置(例如:文件、数据库、在线、隔离存储、注册表) - 可取消应用/持久化属性数据的规则

相信我,这个库非常出色!


18

注册表/配置设置/XML参数仍然非常活跃。随着技术的进步,我已经使用了它们所有,但我的最爱是基于Threed系统独立存储相结合。

以下示例允许将名为“属性”的对象存储到独立存储中的文件中。例如:

AppSettings.Save(myobject, "Prop1,Prop2", "myFile.jsn");

可以使用以下方法恢复属性:

AppSettings.Load(myobject, "myFile.jsn");

这只是一个示例,不代表最佳实践。

internal static class AppSettings
{
    internal static void Save(object src, string targ, string fileName)
    {
        Dictionary<string, object> items = new Dictionary<string, object>();
        Type type = src.GetType();

        string[] paramList = targ.Split(new char[] { ',' });
        foreach (string paramName in paramList)
            items.Add(paramName, type.GetProperty(paramName.Trim()).GetValue(src, null));

        try
        {
            // GetUserStoreForApplication doesn't work - can't identify.
            // application unless published by ClickOnce or Silverlight
            IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForAssembly();
            using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(fileName, FileMode.Create, storage))
            using (StreamWriter writer = new StreamWriter(stream))
            {
                writer.Write((new JavaScriptSerializer()).Serialize(items));
            }

        }
        catch (Exception) { }   // If fails - just don't use preferences
    }

    internal static void Load(object tar, string fileName)
    {
        Dictionary<string, object> items = new Dictionary<string, object>();
        Type type = tar.GetType();

        try
        {
            // GetUserStoreForApplication doesn't work - can't identify
            // application unless published by ClickOnce or Silverlight
            IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForAssembly();
            using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream(fileName, FileMode.Open, storage))
            using (StreamReader reader = new StreamReader(stream))
            {
                items = (new JavaScriptSerializer()).Deserialize<Dictionary<string, object>>(reader.ReadToEnd());
            }
        }
        catch (Exception) { return; }   // If fails - just don't use preferences.

        foreach (KeyValuePair<string, object> obj in items)
        {
            try
            {
                tar.GetType().GetProperty(obj.Key).SetValue(tar, obj.Value, null);
            }
            catch (Exception) { }
        }
    }
}

1
或者更好的方法是使用DataContractJsonSerializer。 - Boczek

18

一个简单的方法是使用配置数据对象,将其保存为以应用程序名称命名的XML文件,并在启动时读取它。

以下是存储窗体位置和大小的示例:

配置数据对象是强类型且易于使用:

[Serializable()]
public class CConfigDO
{
    private System.Drawing.Point m_oStartPos;
    private System.Drawing.Size m_oStartSize;

    public System.Drawing.Point StartPos
    {
        get { return m_oStartPos; }
        set { m_oStartPos = value; }
    }

    public System.Drawing.Size StartSize
    {
        get { return m_oStartSize; }
        set { m_oStartSize = value; }
    }
}

用于保存和加载的管理器类:

public class CConfigMng
{
    private string m_sConfigFileName = System.IO.Path.GetFileNameWithoutExtension(System.Windows.Forms.Application.ExecutablePath) + ".xml";
    private CConfigDO m_oConfig = new CConfigDO();

    public CConfigDO Config
    {
        get { return m_oConfig; }
        set { m_oConfig = value; }
    }

    // Load configuration file
    public void LoadConfig()
    {
        if (System.IO.File.Exists(m_sConfigFileName))
        {
            System.IO.StreamReader srReader = System.IO.File.OpenText(m_sConfigFileName);
            Type tType = m_oConfig.GetType();
            System.Xml.Serialization.XmlSerializer xsSerializer = new System.Xml.Serialization.XmlSerializer(tType);
            object oData = xsSerializer.Deserialize(srReader);
            m_oConfig = (CConfigDO)oData;
            srReader.Close();
        }
    }

    // Save configuration file
    public void SaveConfig()
    {
        System.IO.StreamWriter swWriter = System.IO.File.CreateText(m_sConfigFileName);
        Type tType = m_oConfig.GetType();
        if (tType.IsSerializable)
        {
            System.Xml.Serialization.XmlSerializer xsSerializer = new System.Xml.Serialization.XmlSerializer(tType);
            xsSerializer.Serialize(swWriter, m_oConfig);
            swWriter.Close();
        }
    }
}

现在您可以创建一个实例并在表单的加载和关闭事件中使用:

    private CConfigMng oConfigMng = new CConfigMng();

    private void Form1_Load(object sender, EventArgs e)
    {
        // Load configuration
        oConfigMng.LoadConfig();
        if (oConfigMng.Config.StartPos.X != 0 || oConfigMng.Config.StartPos.Y != 0)
        {
            Location = oConfigMng.Config.StartPos;
            Size = oConfigMng.Config.StartSize;
        }
    }

    private void Form1_FormClosed(object sender, FormClosedEventArgs e)
    {
        // Save configuration
        oConfigMng.Config.StartPos = Location;
        oConfigMng.Config.StartSize = Size;
        oConfigMng.SaveConfig();
    }

生成的 XML 文件也是可读的:

<?xml version="1.0" encoding="utf-8"?>
<CConfigDO xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <StartPos>
    <X>70</X>
    <Y>278</Y>
  </StartPos>
  <StartSize>
    <Width>253</Width>
    <Height>229</Height>
  </StartSize>
</CConfigDO>

1
我在开发环境中已经成功实现了这个功能,但是当我部署应用程序时,普通用户无法访问“c:\program files\my application”文件夹,因此保存设置时会出现错误。我正在考虑将xml文件保存在AppData中,但我想知道是否有明显的解决方法,因为这种方法似乎对您有效。 - Philip Stratford
@PhilipStratford 由于这只是一个普通文件,您可以将其保存在任何地方。只需找到具有写入权限的位置即可。 - Dieter Meemken
@PhilipStratford 可能 AppData 文件夹对您来说是一个选项,请参阅 C# 获取 %AppData% 路径,正如 kite 所提到的。 - Dieter Meemken
谢谢,我已经实现了这个功能,将xml文件保存在AppData文件夹中。我只是想知道是否有一种简单的方法可以像您的示例那样将其保存在应用程序文件夹中,因为我认为您已经使其工作了。不用担心,AppData文件夹可能是更好的位置! - Philip Stratford

18
是的,可以保存配置,但这在很大程度上取决于您选择的方式。让我描述一下技术上的差异,以便您了解您拥有的选项:
首先,您需要区分是否要在*.exe.config文件中使用applicationSettings或AppSettings(在Visual Studio中称为App.config) - 这里有根本性的差异,正在此处描述
两者都提供不同的保存更改方式:
  • AppSettings允许您通过config.Save(ConfigurationSaveMode.Modified);直接读写配置文件,其中config定义为:
    config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
  • applicationSettings允许读取,但如果您进行更改(通过Properties.Settings.Default.Save();),它将按用户基础写入,存储在特定位置(例如C:\Documents and Settings\USERID\Local Settings\Application Data\FIRMNAME\WindowsFormsTestApplicati_Url_tdq2oylz33rzq00sxhvxucu5edw2oghw\1.0.0.0)。正如Hans Passant在他的答案中提到的那样,这是因为用户通常对程序文件具有受限权限,不能在不调用UAC提示的情况下写入它。缺点是,如果将来要添加配置键,则需要与每个用户配置文件同步。

但还有一些其他替代选项:

自从.NET Core(以及.NET 5和6)出现后,第三个选项是使用Microsoft的配置抽象的appsettings.json文件(以及存储在用户资料中而不是程序集目录中的secrets.json文件)。但通常WinForms不会使用它,所以我只是为了完整性而提到它。然而,这里有一些关于如何读取写入这些值的参考。或者你可以使用Newtonsoft JSON来读取和写入appsettings.json文件,但它不仅限于此:你还可以用这种方法创建自己的json文件。
正如问题中提到的,第四个选项是:如果你将配置文件视为XML文档,你可以使用System.Xml.Linq.XDocument类加载、修改和保存它。不需要使用自定义XML文件,你可以读取现有的配置文件;对于查询元素,甚至可以使用Linq查询。我在这里给出了一个示例,请查看答案中的GetApplicationSetting函数。
第五个选项是将设置存储在注册表中。如何实现可以在这里找到说明。
最后,第六个选项是:你可以将值存储在环境变量中(系统环境或你的账户环境)。在Windows设置中(Windows菜单中的齿轮),在搜索栏中输入"environment"并添加或编辑它们。要读取它们,使用
var myValue = Environment.GetEnvironmentVariable("MyVariable");
请注意,你的应用程序通常需要重新启动才能获取更新的环境设置。
如果您需要加密来保护您的数据,请查看this答案。它描述了如何使用Microsoft的DPAPI存储加密值。
如果您想支持自己的文件,无论是XML还是JSON,了解运行程序集的目录可能会很有用:
var assemblyDLL = System.Reflection.Assembly.GetExecutingAssembly();
var assemblyDirectory = System.IO.Path.GetDirectoryName(assemblyDLL.Location);

你可以使用{{assemblyDirectory}}作为基础目录来存储你的文件。

9
“这是否意味着我应该使用自定义的XML文件来保存配置设置?” 不一定。我们使用SharpConfig进行此类操作。
例如,如果配置文件是这样的:
[General]
# a comment
SomeString = Hello World!
SomeInteger = 10 # an inline comment

我们可以像这样检索值。
var config = Configuration.LoadFromFile("sample.cfg");
var section = config["General"];

string someString = section["SomeString"].StringValue;
int someInteger = section["SomeInteger"].IntValue;

它与.NET 2.0及更高版本兼容。我们可以动态创建配置文件,并随后保存它。
来源:http://sharpconfig.net/
GitHub: https://github.com/cemdervis/SharpConfig

非常好的答案,救了我的一天。谢谢!如何使用代码修改那个文件,假设我需要添加更多参数?再次感谢。 - Moiyd
1
请参阅 https://github.com/cemdervis/SharpConfig 中的“在内存中创建配置”和“保存配置”。 - Turker Tunali

8

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