在上一篇中的NumberTextBox控件中用到了一个NumberRange属性 用来设置一个数据的范围
private int m_nHigh = 100; private int m_nLow = 0; private string _NumberRange = "0,100"; [DefaultValue("0,100")] public string NumberRange { get { return _NumberRange; } set { _NumberRange = value; }//转换步骤略 参见上一篇 }不过看上去似乎有点别扭 看上去怪怪的 为什么不直接就把上面的两个封装成属性 却要用一个字符串来作为属性 然后再拆分 转换 赋值 因为想要的效果像控件的Size属性那样去设置属性 但是很明显的 控件的Size属性是一个struct结构、、到这里 或许你想到 既然如此那么这里也把NumberRange搞成结构就行了啊 好吧 那试试看看效果:
public class NumberTextBox : TextBox { private NumberRange _NumberRange; public NumberRange NumberRange { get { return _NumberRange; } set { _NumberRange = value; } } } public struct NumberRange { public int Low; public int High; }然后把控件添加到窗体上看看属性效果:
显示结果似乎不是想要的结果
因为vs不知道你这个到底是个啥东西 它不知道要怎么去显示 通常情况下vs属性窗口只会显示他知道的东西 比如:int bool 颜色 集合 字符串 枚举 什么的 已知类型 这里的struct是用户自己定义的一个类型 vs不知道这个类型在属性窗口该要怎么显示 所以 要告诉vs怎么去处理这个东西 于是乎TypeConverter 出场了 现在把struct重新写一下:
[TypeConverter(typeof(NumberRangeConverter))] public struct NumberRange { private int _Low; public int Low { get { return _Low; } set { _Low = value; } } private int _High; public int High { get { return _High; } set { _High = value; } } public NumberRange(int l, int h) { this._Low = l; this._High = h; } }其实比起刚才的 我只是把里面的变量封装成了属性而已 然后上面多了一个特性 上面的struct中为什么全是public 而下面的就用的属性的方式了呢?好吧其实正常情况下 都应该想下面的这个这样写 struct 和 class 上的写法上几乎没有任何区别 struct是值类型存在栈上 而class是引用类型 存在堆上 其实一般情况下我很少用struct这个东西 在调用Win32Api的时候 一些需要结构进去的我就是用的struct 在声明struct的时候 通常都是public ... 因为简洁 所以我在struct里面都不搞属性 只是仅仅作为一个结构交换数据用 所以提醒一下 和我有同样习惯的人 如果这里 你还是像上面一样的写法 估计后面会出一点问题 后面再说
然后值得一提的是上面的特性 当然这还没完 因为你编译会报错、、因为NumberRangeConverter不存在 是的、、现在要做的就是写一个这个类 、、因为就是这个类来告诉vs要怎么处理这个类型:
public class NumberRangeConverter : TypeConverter { //这个类将完成属性窗口和对象之间的转换 }命名规则XXXConverter 然后这个类继承TypeConverter 值得关注的是 里面有四个要重写的函数
public override bool CanConvertFrom( ITypeDescriptorContext context, Type sourceType) { if (sourceType == typeof(string)) return true; return base.CanConvertFrom(context, sourceType); }因为到时候在属性窗口上 将以字符串的形式给出值 所以允许从字符串转换
public override bool CanConvertTo( ITypeDescriptorContext context, Type destinationType) { if (destinationType == typeof(string)) return true; if (destinationType == typeof(InstanceDescriptor)) return true; return base.CanConvertTo(context, destinationType); }表示可以转换成一个字符串类型或者一个实例描述 总感觉上面两个亘古不变的 因为属性窗口那里输入过来的基本都是以字符串的形式 其实我也有点不太理解这两个东西 但是资料比较难找 所以勉强懂个大概 我还是吧MSDN上TypeConverter的链接贴出来吧 TypeConverter传送门
public override object ConvertFrom( ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) { if (value is string) { string[] v = ((string)value).Split(','); if(v.Length != 2) throw new ArgumentException("Error"); try{ return new NumberRange(int.Parse(v[0]), int.Parse(v[1])); } catch(Exception ex) { throw ex; } } return base.ConvertFrom(context, culture, value); }这里就开始转换了 这个是把用户输入的字符串转换成结构对象的 是从属性窗口到对象的转换 因为输入是按照[num1,num2]的形式输入的 所以这里面的代码相信都知道是啥意思 下面来看最后一个:
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType) { NumberRange nr = (NumberRange)value; if (destinationType == typeof(string)) { return nr.Low + "," + nr.High;//这个会显示在属性窗口上 } if (destinationType == typeof(InstanceDescriptor)) { return new instancedescriptor(//生成XXX.Designer.cs里面的代码 typeof(numberrange).getconstructor(new type[] { typeof(int), typeof(int) }), new int[] { nr.low, nr.high } );//里面分别是 构造器 和 要给构造器的值 } return base.ConvertTo(context, culture, value, destinationType); }这里需要注意的就是第二个判断里面的代码 就是让vs生成xxx.Designer.cs里面的代码 在里面如何构造这个对象 因为NumberRange这里只定义了一个构造器 两个int参数 如果你还有其他构造器 也可以使用 只要你构造器选择对就行了 比如要是我有一个这个构造器:
public NumberRange(int l, string strH) { this._Low = l; this._High = int.Parse(strH); }第二个是接收字符串类型的 我也可以在ConverTo里面这样:
return new InstanceDescriptor( typeof(NumberRange).GetConstructor(new Type[] { typeof(int), typeof(string) }), new object[] { nr.Low, nr.High.ToString() } );
代码先到这里告一个段落 先来看看效果再继续说 不然全部说完了再来讲 估计有点不知所云 估计现在连上面这四个是啥都还不知道
先把控件添加到窗体 当然 你可以给属性来个默认值:
public class NumberTextBox : TextBox { private static readonly NumberRange defaultNumberRangeValue = new NumberRange(0, 100); private NumberRange _NumberRange = defaultNumberRangeValue; public NumberRange NumberRange { get { return _NumberRange; } set { _NumberRange = value; } } public bool ShouldSerializeNumberRange() { return !this._NumberRange.Equals(defaultNumberRangeValue); } public void ResetNumberRange() { this._NumberRange = defaultNumberRangeValue; } }这个在前面属性的文章的时候说过的 魔术命名 然后把控件添加到窗体上看看:
能看到 已经可以正确显示了 只是和Size的比起来 还不能打开
然后修改一下属性看看效果 xxx.Designer.cs 里面的代码:
可以看到 这里是怎么给NumberRange属性赋值的 我这里用的是上面刚才随口说的那个构造器 然后这里是生成的对应的代码
然后 有了一点效果之后 继续让他真么能像 Size 属性一样还可以展开
其实很简单 继续在NumberRangeConverter里面重写两个方法就行了:
public override bool GetPropertiesSupported( ITypeDescriptorContext context) { return true;//允许显示子属性 } public override PropertyDescriptorCollection GetProperties( ITypeDescriptorContext context, object value, Attribute[] attributes) { //返回属性列表 所以上面说 如果struct里面不写成属性 直接用public这里就获取不到属性 return TypeDescriptor.GetProperties(typeof(NumberRange), attributes); }就像上面这样 然后你就可以看到 属性能够展开了
到此 本篇基本结束 如果你的子属性又要在属性窗口显示并他也是一个自定义的类型的话就可以用TypeConverter来进行转换 当然不只是对struct才能用class也一样可以 不过在[C#GDI+编程]中还提到 不变类的处理
"一个不变的类(如String)的对象一旦创建就永远不能被修改 就是说 如果在Properties窗口中编辑一个不变类的属性值 vs不会修改对象 它将不得不删除那个对象并用新值创建一个新对象" 但是这里的NumberRange不是不变的 对于说道的不变类的情况 我本身没有遇到过 所以只是照搬书上的来说 如果要启用vs来销毁和重建包含一个不变复合属性的对象 重写两个方法即可:public override bool GetCreateInstanceSupported( ITypeDescriptorContext context) { return true; } public override object CreateInstance(ITypeDescriptorContext context, System.Collections.IDictionary propertyValues) { return new NumberRange( int.Parse(propertyValues["Low"].ToString()), int.Parse(propertyValues["High"].ToString()) ); }
像上面的方式即可 不过这里不用 还有一个问题我不得不说、、
如果说 你代码确实是按照上面写的 如果出问题 啥问题?上个截图:
(这个Thickness是另外写的一个属性 别人的代码)
如果你这里提示的是 NumberRange 报错说是 指定转换无效 那么 我也不知道是啥问题 这篇文章本来应该是昨天出的 结果昨天我在写Demo的时候 就出现了这个问题 困扰了我一个晚上 开始我一直以为是我代码那里写的有问题 然后又把TypeConverter里面的代码全部删了 再写 结果还是这样的问题 然后我就有点郁闷了 这不科学 百度不到结果(这东西本来就少 如果是问题就更少了) 后来再谷歌 貌似国外的一个师兄也遇到过这个问题 这个是连接:
http://stackoverflow.com/questions/19965502/implementing-typeconverter-for-windows-forms
然后看到下面的回答 我不淡定了:
当时 我只看到Rename 没错是的 Rename 然后我看着就有点不科学 然后 我就去吧我的NumberRangeConverter随便改成了NumberRange111Converter、、(0.0)、、真的不报错了 这不科学、、、
然后第二位回答的师兄说 在他那里是对的 然后我也好奇的把提问者贴出来的代码 放我的工程里面 确实没有出现问题 然后就这样了吧 就在刚才 我打开工程确认的时候 发现我改了类名的又报错了 然后我又改回了NumberRangeConverter然后好了 可是昨天都还是好好的那位提问师兄的代码却不正常了 保存编辑后的属性的时候出现了刚才截图的那个提示框 就是那位师兄的代码 昨天都还是好好的今天就不正常了 太不科学了
如果有那位知道这是怎么回事 请一定指教啊 我是vs2010 .Net 2.0 记得以前都没有出现这问题来着 不知道 到底是环境问题 还是真是代码问题 一个不小心哪里被我忽略了、、