使用 PropertyGrid 的 C# 用户首选项






4.23/5 (13投票s)
在 C# 中使用 PropertyGrid 控件实现用户首选项表单。
引言
本程序演示了如何实现一个用户首选项属性网格控件,该控件使用任意字符串名称,并与 VS2005 的“项目设置”页面协同工作,以创建和更新 user.config 文件。
背景
我当时正在用 C# 编写我的第一个大型应用程序(一个科学可视化程序),到了需要实现“用户首选项”功能的时候。在我之前用 C++ 做的项目中,我使用树视图控件将首选项组织成类别,但这一次,我想使用新的“PropertyGrid
”控件和 VS2005 中的“项目设置”功能。
VS2005 中的“项目设置”功能非常酷。你可以通过如下所示的对话框将设置定义为“应用程序”设置或“用户”设置。这个工具了解你可能需要的所有不同对象类型,包括字符串、字体、颜色等,并为每一种类型提供合适的编辑器——非常巧妙!此外,IDE 还施展了一些魔法,使得每次应用程序终止时,设置都会被保存到磁盘上的 XML 文件中,然后在下次应用程序启动时从磁盘加载这些设置。应用程序设置会保存到与可执行文件同目录下的一个名为“app.config”的文件中,而用户设置则保存到用户“Local Settings\ApplicationData”文件夹下的另一个名为“user.config”的文件中(注意:这 *不是* 用户名文件夹正下方的“Application Data”文件夹!)。这种魔法的一部分是自动生成的“Settings
”类,它由一个 Settings.cs 文件和一个 Settings.Designer.cs 文件组成。Settings.Designer.cs 文件包含了为每个项目设置自动生成的类私有成员和相关的公共属性,以及每个设置对应的属性(attribute)条目。
PropertyGrid
控件也非常酷。只需将其“SelectedObject
”属性设置为对“项目设置”功能自动创建的 Settings
类对象的引用,你就能以一种非常好的方式向用户展示项目设置。用户可以方便地编辑用户范围(但不能编辑应用程序范围)的设置,并且任何更改在应用程序终止时都会被写出到相应的 .config 文件中。然而,不幸的是,PropertyGrid
/“项目设置”的这种魔法有一个主要缺陷——你只能在“项目设置”网格中使用有效的符号名称,因此用户看到的是像“default_plot_font”这样的设置名称,而不是“二维图表的默认字体”。幸运的是,多亏了 Tony Allowatt 的文章 《随心所欲地改造 .NET PropertyGrid》,有一种方法可以“欺骗”PropertyGrid
,让它显示任意字符串而不是属性名称。
虽然 Tony 的文章非常有帮助,但我仍然需要做一些工作和大量的思考,才能做到用友好的字符串标题向用户展示用户范围的项目设置。完成之后,我觉得有必要贡献一个小项目,来完整地展示“项目设置”——用户编辑——文件存储的整个流程。
代码详情
下面的代码片段展示了主窗体(包含 PropertyGrid
控件的那个窗体)的 Load
和 btnOK_Click
事件处理程序。当你第一次使用“项目->设置”表单创建新的项目设置时,系统会为你输入的每个属性创建一个属性定义,并为任何“用户”设置的属性定义添加“System.Configuration.UserScopedSettingAttribute
”特性。自动生成的 Settings
类还处理了所有将设置写入/读出 user.config 的机制。
Load
处理程序使用 .NET 的反射机制来解析 Settings
类中的所有属性,寻找带有上述特性的属性。首先,它获取对静态 Settings
对象 EMWorkbench.Properties.Settings.Default
(也是自动创建的)的引用,然后获取 Settings
类的 Default
对象的所有属性列表,接着对每个具有 UserScopedSettingAttribute
特性的属性,使用一个 case
语句创建一个 PropertySpec
对象,并将其添加到 PropertyTable
表中(PropertySpec
和 PropertyTable
类由 Tony Allowatt 提供)。PropertySpec
类的 Name
字段允许我们将一个正式的属性名,如 'prefs_files_bscexec',翻译成一个更友好的字符串,如“NEC-BSC CEM 代码位置”。
“确定”按钮的 Click
处理程序则反向操作,将用户在“首选项”页面上的任何更改反馈给静态的默认 Settings
类对象。然后,Settings
类负责将更改写回到 user.config XML 文件中。
“确定”按钮的 Click
处理程序会沿着 PropertyGrid
结构向上回溯,直到获取到“根”元素的引用。然后,它对所有顶层网格项调用 ParseGridItems(item)
,而 ParseGridItems
负责将首选项项的值传回给 Settings
类。请注意,ParseGridItems(item)
是一个递归函数,它会根据需要多次调用自身,以遍历 PropertyGrid
的树形结构。当 ParseGridItems()
找到一个不属于“Category”类型的网格项时,它会终止递归,并在 case
语句中寻找匹配的属性描述字符串。如果找到匹配项,相应的 Settings
类属性就会被更新为当前值。
private void Form_Prefs_Load(object sender, EventArgs e)
{
//01/20/09 build table of properties using current values from Settings object
// all this is required because PropertyGrid doesn't allow arbitrary string property
// names by default.
// Uses the PropertySpec & PropertyTable classes from PropertyBag.cs,
// courtesy of Tony Allowatt
// the basic constructor signature is:
// public PropertySpec(string propname, string name, string type, string category,
// string description, object defaultValue, Type editor, Type typeConverter)
// 'propname' values come from Settings.Designer.cs, which is managed automatically
// by the project settings dialog
//create & fill the table.
PropertyTable proptable = new PropertyTable();
//Construct PropertyTable entries from Settings class user-scoped properties
UserPrefs.Properties.Settings settings = UserPrefs.Properties.Settings.Default;
Type type = typeof(Properties.Settings);
MemberInfo[] pi = type.GetProperties();
foreach (MemberInfo m in pi)
{
Object[] myAttributes = m.GetCustomAttributes(true);
if (myAttributes.Length > 0)
{
for (int j = 0; j < myAttributes.Length; j++)
{
if( myAttributes[j].ToString() ==
"System.Configuration.UserScopedSettingAttribute")
{
PropertySpec ps = new PropertySpec("property name",
"System.String");
switch (m.Name)
{
//Files category
case "prefs_files_bscexec":
ps = new PropertySpec(
"Bsc CEM Code",
"System.String",
"File Locations",
"NEC-BSC CEM Code Location",
settings.prefs_files_bscexec.ToString(),
typeof(System.Windows.Forms.Design.FileNameEditor),
typeof(System.Convert));
break;
//Colors
case "pec_color":
ps = new PropertySpec(
"PEC Color",
typeof(System.Drawing.Color),
"Colors",
"Color used for PEC model elements",
settings.pec_color);
break;
//Fonts
case "default_plot_font":
ps = new PropertySpec(
"Default Plot Font",
typeof(Font),
"Fonts",
"Default font used for 2-D plots",
settings.default_plot_font);
break;
}
proptable.Properties.Add(ps);
}
}
}
}
//this line binds the PropertyTable object to the preferences PropertyGrid control
this.pg_Prefs.SelectedObject = proptable;
}
private void btn_OK_Click(object sender, EventArgs e)
{
//write property values back to Settings object properties
Button btn = (Button)sender;
Form_Prefs form = (Form_Prefs)btn.Parent;
PropertyGrid pg = form.pg_Prefs;
PropertyTable proptable = pg.SelectedObject as PropertyTable;
//EMWorkbench.Properties.Settings settings = EMWorkbench.Properties.Settings.Default;
//get the grid root
GridItem gi = pg.SelectedGridItem;
while (gi.Parent != null)
{
gi = gi.Parent;
}
//transfer all grid item values to Settings class properties
foreach( GridItem item in gi.GridItems)
{
ParseGridItems(item); //recursive
}
this.Close();
}
private void ParseGridItems(GridItem gi)
{
UserPrefs.Properties.Settings settings = UserPrefs.Properties.Settings.Default;
if (gi.GridItemType == GridItemType.Category)
{
foreach (GridItem item in gi.GridItems)
{
ParseGridItems(item); //terminates at 1st Property
}
}
switch (gi.Label)
{
case "Bsc CEM Code":
settings.prefs_files_bscexec = gi.Value.ToString();
break;
case "PEC Color":
settings.pec_color = (Color)gi.Value;
break;
case "Default Plot Font":
settings.default_plot_font = (Font)gi.Value;
break;
default:
break;
}
}
Using the Code
要使用此代码,只需启动可执行文件。一个带有 PropertyGrid
控件和“确定”/“取消”按钮的窗体将会出现,并显示三个属性。修改一个或多个属性,然后点击“确定”将更改保存到 user.config 文件。点击“取消”将退出程序而不将更改写入磁盘。
关注点
关于“UserPrefs”项目的一些补充说明。
- 这行代码
UserPrefs.Properties.Settings.Default.Save();
必须在应用程序退出时执行。没有它,user.config 文件就不会被创建(或更新)。我把这个调用放在 Program.cs 文件中Run()
调用之后,但它可以放在程序关闭时会执行的任何地方。 - 起初,当我试图为一个文件位置实现用户设置时遇到了困难,因为我不知道该指定哪个编辑器对象。我最终找到了一个指向“
System.Windows.Forms.Design.FileNameEditor
”的帖子,但即使在类文件中添加了“System.Windows.Forms.Design
”的using
语句,代码也无法编译。解决方法是,在解决方案资源管理器中右键单击项目名称下的“引用”,选择“添加引用…”,然后从列表中选择“System.Design”,从而将“System.Design
”添加为项目引用。 - 我没能想出一个真正简单的方法来避免两个“
case
”代码块(一个用于加载PropertyGrid
使用的表,另一个用于从PropertyGrid
更新Settings
类的属性)。因此,添加一个新的用户范围属性需要以下步骤: - 通过“项目 -> 设置”对话框添加新属性,并将“范围”字段选择为“用户”。
- 在
Form_Prefs_Load()
中添加一个新的“case
”块,包含所需的显示名称、描述以及(如果需要)编辑器和/或转换对象。Tony 的PropertyBag
类提供了多个构造函数,可以处理几乎任何可以想象的组合。请注意,“case <此处的字符串>:
”中的字符串必须是在“项目设置”对话框中定义的属性名称。 - 在
Form_Prefs_Btn_OK_Click()
中为每个PropertyGrid
项添加一个新的“case
”块,以更新Settings
类的属性。在这里,“case <此处的字符串>:
”中的字符串必须是在Form_Prefs_Load()
中定义的显示名称。 - user.config 文件的位置在调试版本和发布版本中是不同的,甚至在调试(F5)模式下运行的发布版本中也可能不同。在我的系统上,调试版本会将 user.config 文件放在 C:\Documents and Settings\Frank\Local Settings\Application Data\UserPrefs\UserPrefs.vshost.exe_Url_m1s4iatnmp4yoimwn3wgal4p4kemi4u4\1.0.0.0。注意路径名中的“vshost.exe”——这表明它是调试版本(或至少是在调试模式下运行)。如果程序是通过双击 Bin\Release 文件夹中的 UserPrefs.exe 启动的,那么文件会放在 C:\Documents and Settings\Frank\Local Settings\Application Data\UserPrefs\UserPrefs.exe_Url_mzddu0ljbs451jxuvpl3xqzfdbzjtgwx\1.0.0.0。