零.引言
PropertyGrid用來顯示和編輯對象的屬性,前面已經簡單介紹了如何使用該控件和提供不同的屬性編輯方法。前面主要講如何使用該控件,但有時,該控件無法滿足我們的需求,就需要對其進行擴展。本文主要介紹如何在PropertyGrid中添加屬性選項卡(PropertyTab)。VS自帶的屬性框有屬性和事件兩個屬性卡,下面簡單說明如何添加自己的選項卡。
一.PropertyGrid的組成
在添加選項卡之前,先來看一看PropertyGrid的組成,分析其組成對后面設計十分有用。微軟將PropertyGrid封裝的十分好了,使用起來十分的方便,但是其具體如何實現的卻不得而知。翻看MSDN,找到一點皮毛。
首先看一下PropertyGrid類,比較復雜,發現其有一個PropertyTabs屬性,應該是PropertyGrid所包含的選項卡集合,其類型是PropertyGrid.PropertyTabCollection,很顯然它是一個PropertyTab集合類。接着看一下PropertyTab類,下面是MSDN中的描述:
- 為屬性選項卡提供基類
- PropertyTab 類為屬性選項卡提供基類行為。屬性選項卡顯示在“屬性窗口”的 PropertyGrid 控件的工具欄上,並且允許組件顯示其屬性或其他數據的不同視圖。
- 用戶代碼通常不會直接創建 PropertyTab 的實例。而可以將一個 PropertyTabAttribute(它指示為某一組件顯示的一個或多個屬性選項卡的類型)與應該顯示 PropertyTab 的屬性或類型相關聯。
- PropertyGrid 將實例化一個與所瀏覽組件的類型或屬性字段關聯的 PropertyTabAttribute 所指定類型的 PropertyTab。
很顯然,我們創建選項卡就是要以它為基類。看一下他的成員,還好,比較簡單,只有四個屬性,其中有一個Bitmap,是用來設置選項卡的圖標的,一個是TabName,很明顯是選項卡名稱,還有一個GetProperties方法,非常的重要,獲取指定組件的屬性信息集合,返回的是PropertyDescriptorCollection類型,很顯然他是一個PropertyDescriptor的集合。請記住這三個成員,后面會有用到的。
既然返回了PropertyDescriptor,我們就來看一看它,PropertyDescriptor類是描述屬性信息的一個類,接着看,看PropertyDescriptor是如何描述屬性信息的,MSDN中這樣描述的:
屬性的說明由名稱、其特性、與該屬性關聯的組件類和該屬性的類型組成。
PropertyDescriptor 提供以下屬性和方法:
- Converter 包含此屬性的 TypeConverter。
- IsLocalizable 指示該屬性是否應該本地化。
- GetEditor 返回指定類型的編輯器。
PropertyDescriptor 還提供以下 abstract 屬性和方法:
- ComponentType 包含該屬性綁定到的組件的類型。
- IsReadOnly 指示該屬性是否是只讀的。
- PropertyType 獲取屬性的類型。
- CanResetValue 指示重置組件是否會更改該組件的值。
- GetValue 返回組件上屬性的當前值。
- ResetValue 重置組件屬性的值。
- SetValue 將組件的值設置為一個不同的值。
- ShouldSerializeValue 指示是否需要持久保存該屬性的值。
通常,abstract 成員是通過反射實現的。有關反射的更多信息,請參見 反射 中的主題。
可見它非常詳細的描述了一個屬性的信息。而且從中好像看到了很多熟悉的東西,類型轉換,指定編輯器,設計時序列化,是的,在前面幾篇文章中介紹的一些功能,其實在內部就是通過這個類來實現的。
再重新仔細看一下PropertyGrid類,發現有一個SelectedGridItem的屬性,返回的是GridItem類型,MSDN如是說:
每個 GridItem 都對應於 SelectedObject 的一項屬性。
可以使用返回的 GridItem 查看選定對象的類型信息、PropertyDescriptor、父對象和子對象。
看一下這個GridItem。MSDN是這樣寫道的:
實現 PropertyGrid 中的一行。
網格項將視圖的層次結構表示為 PropertyGrid。可以使用 GridItem 獲取關於網格狀態和內容的信息。
不應緩存 GridItem 對象,因為它們表示訪問它們時 PropertyGrid 的狀態的快照,而且網格活動可能釋放它們。PropertyGrid 經常內部重新創建 GridItem 對象,而不更改呈現給用戶的視圖。
也就是說PropertyGrid的視圖是由一個一個GridItem壘起來的,而且只是狀態的快照,你在改變屬性排序,展開,折疊等的時候,PropertyGrid可能就會釋放,創建GridItem。來看一下GridItem的組成,他有一個GridItems屬性,說明GridItem是可以有子項的,也就是屬性中的子屬性;還有一個PropertyDescriptor屬性,就是上面PropertyTab中GetProperties返回來的類型,他用來描述與此GridItem相連的屬性的信息,以便將屬性顯示出來。
下面這張圖簡單描述PropertyGrid的組成:
從以上的分析,大致可以看出PropertyGrid是如何工作的,當把某一對象賦值給PropertyGrid的SelectedObject屬性時,PropertyGrid通知選中的PropertyTab,PropertyTab通過他的GetProperties方法,獲取並返回這個對象的一個屬性信息集合(PropertyDescriptorCollection),PropertyGrid獲取到這個集合后,分析其內容,為集合中的每個元素(每個屬性)創建一個GridItem,並將GridItem組織起來顯示出來,也就是我們所看到的屬性內容了。
由此可見,我們對屬性的操作是通過GridItem進行的,而GridItem又通過PropertyDescriptor來對屬性進行操作,在PropertyDescriptor中會看到很多我們之前見到過的東西,如類型轉換,屬性編輯器,屬性設計時序列化等,其實這些都是通過PropertyDescriptor來實現的(詳情可見MSDN)。
三.添加屬性選項卡(PropertyTab)
了解了PropertyGrid的組成后,再來添加選項卡就很清晰了。首先我們知道要新建一個選項卡類,並且要繼承於PropertyTab。重寫PropertyTab的TabName和Bitmap屬性,用來設置選項卡的外觀和名字;重寫GetProperties方法,來設置我們這個選項卡中要顯示哪些屬性,只需返回一個我們要顯示的屬性的PropertyDescriptorCollection,至於后面創建GridItem和顯示的工作就交給PropertyGrid自己去完成就行了。
下面舉個例子:同樣的,假如我們有一個控件類:

1 public class MyControl : System.Windows.Forms.UserControl 2 { 3 private double _angle = 90; 4 5 public MyControl() 6 { 7 } 8 9 [BrowsableAttribute(true)] 10 [Category("角度")] 11 public double Angle 12 { 13 get 14 { return _angle; } 15 set 16 { _angle = value; } 17 } 18 19 20 protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) 21 { 22 e.Graphics.DrawString("The Angle is " + _angle, this.Font, Brushes.Red, 0, 0); 23 } 24 }
控件類有一個Angle的屬性,以及它從UerControl繼承過來的很多屬性,在VS的屬性框中會全部的顯示出來,如果現在我們只需要顯示在“角度”這一分類中的([Category("角度")])屬性。我們新建一個選項卡類,讓他只顯示“角度”這一分類中的屬性。

1 //自定義選項卡類 2 [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] 3 public class MyPropertyTab : PropertyTab 4 { 5 [BrowsableAttribute(true)] 6 //位圖文件 7 private string img = "AAEAAAD/////AQAAAAAAAAAMAgAAAFRTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj0xLjAuMzMwMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABVTeXN0ZW0uRHJhd2luZy5CaXRtYXABAAAABERhdGEHAgIAAAAJAwAAAA8DAAAA9gAAAAJCTfYAAAAAAAAANgAAACgAAAAIAAAACAAAAAEAGAAAAAAAAAAAAMQOAADEDgAAAAAAAAAAAAD///////////////////////////////////9ZgABZgADzPz/zPz/zPz9AgP//////////gAD/gAD/AAD/AAD/AACKyub///////+AAACAAAAAAP8AAP8AAP9AgP////////9ZgABZgABz13hz13hz13hAgP//////////gAD/gACA/wCA/wCA/wAA//////////+AAACAAAAAAP8AAP8AAP9AgP////////////////////////////////////8L"; 8 9 public MyPropertyTab() 10 { 11 } 12 13 //返回我們要顯示的屬性的信息集合 14 public override System.ComponentModel.PropertyDescriptorCollection GetProperties(object component, System.Attribute[] attributes) 15 { 16 //獲取對象所有屬性 17 PropertyDescriptorCollection props; 18 if (attributes == null) 19 props = TypeDescriptor.GetProperties(component); 20 else 21 props = TypeDescriptor.GetProperties(component, attributes); 22 23 //篩選“角度“屬性 24 int angleCount = 0; 25 for (int i = 0; i < props.Count; i++) 26 { 27 if (props[i].Category == "角度") 28 { 29 angleCount++; 30 } 31 } 32 PropertyDescriptor[] ps = new PropertyDescriptor[angleCount]; 33 int j = 0; 34 for (int i = 0; i < props.Count; i++) 35 { 36 PropertyDescriptor des = props[i]; 37 if (des.Category == "角度") 38 { 39 ps[j] = des; 40 j++; 41 } 42 } 43 44 return new PropertyDescriptorCollection(ps); 45 } 46 47 public override System.ComponentModel.PropertyDescriptorCollection GetProperties(object component) 48 { 49 return this.GetProperties(component, null); 50 } 51 52 // Tab的名字. 53 public override string TabName 54 { 55 get 56 { 57 return "AngleProperty"; 58 } 59 } 60 61 // Tab的圖標 62 public override System.Drawing.Bitmap Bitmap 63 { 64 get 65 { 66 Bitmap bmp = new Bitmap(DeserializeFromBase64Text(img)); 67 return bmp; 68 } 69 } 70 71 // 從字符串中獲取位圖 72 private Image DeserializeFromBase64Text(string text) 73 { 74 Image img = null; 75 byte[] memBytes = Convert.FromBase64String(text); 76 IFormatter formatter = new BinaryFormatter(); 77 MemoryStream stream = new MemoryStream(memBytes); 78 img = (Image)formatter.Deserialize(stream); 79 stream.Close(); 80 return img; 81 } 82 83 84 }
可見我們重寫了GetProperties方法,該方法從對象的屬性中篩選出“角度”類屬性;重寫了TabName,讓其為AngleProperty;重寫Bitmap,是選項卡的圖標,這里是從字符串中獲取的位圖,也可以從項目資源中獲取。
設計好選項卡后,如何讓它出現在PropertyGrid中呢,有兩種方法:
1. 如果這個選項卡只是針對我們一個特定的類,就如上面那個例子,選項卡顯示“角度”類型屬性,並不是所有的類都有“角度”屬性,選項卡只對有“角度”的類型有意義,那么,我們就讓它在選中了那樣的類型時才顯示,不然就不顯示,怎么做呢,很簡單,只需要給那樣的類(有“角度”屬性的類)添加一個特性:PropertyTab特性,例如在我們的MyControl類型上加上
[PropertyTabAttribute(typeof(MyPropertyTab), PropertyTabScope.Component)]
第一個參數中是我們自己定義的選項卡類,注意這里第二個參數使用的是 PropertyTabScope.Component,它表明只對這種類型的屬性使用MyPropertyTab選項卡。PropertyTabScope.Document表明只要在此文檔中,屬性選項卡就一直顯示(MSDN中說得,一直不太明白什么意思)。其他兩個不能使用。
2. 如果這個選項卡是針對所有類型的,他需要一直顯示,那么我們就直接把他加到我們的PropertyGrid控件中。上面說了PropertyGrid有個PropertyTabs的屬性,將我們自定義的選項卡類添加到這個集合中即可,例如:
this.propertyGrid1.PropertyTabs.AddTabType(typeof(MyPropertyTab));
當然,這樣添加后,他針對所有的類型的對象都有效,就不需要再給特定的類添加PropertyTab特性了。
本例中,我們使用第一種方法,添加PropertyTab特性。即在我們定義的MyControl類上加上[PropertyTabAttribute(typeof(MyPropertyTab), PropertyTabScope.Component)]。好了,下面讓我們來看一下效果:
可見我們自定義的選項卡顯示在PropertyGrid中了,而且只顯示了“角度”類屬性。
四.完整代碼
以下是完整的代碼:

1 using System; 2 using System.ComponentModel; 3 using System.ComponentModel.Design; 4 using System.Drawing; 5 using System.IO; 6 using System.Reflection; 7 using System.Runtime.Serialization; 8 using System.Runtime.Serialization.Formatters.Binary; 9 using System.Windows.Forms; 10 using System.Windows.Forms.Design; 11 using System.Collections.Generic; 12 13 namespace TestAddTab 14 { 15 //控件類 16 [PropertyTabAttribute(typeof(MyPropertyTab), PropertyTabScope.Component)] 17 public class MyControl : System.Windows.Forms.UserControl 18 { 19 private double _angle = 90; 20 21 public MyControl() 22 { 23 } 24 25 [BrowsableAttribute(true)] 26 [Category("角度")] 27 public double Angle 28 { 29 get 30 { return _angle; } 31 set 32 { _angle = value; } 33 } 34 35 36 protected override void OnPaint(System.Windows.Forms.PaintEventArgs e) 37 { 38 e.Graphics.DrawString("The Angle is " + _angle, this.Font, Brushes.Red, 0, 0); 39 } 40 } 41 42 //自定義選項卡類 43 [System.Security.Permissions.PermissionSet(System.Security.Permissions.SecurityAction.Demand, Name = "FullTrust")] 44 public class MyPropertyTab : PropertyTab 45 { 46 [BrowsableAttribute(true)] 47 //位圖文件 48 private string img = "AAEAAAD/////AQAAAAAAAAAMAgAAAFRTeXN0ZW0uRHJhd2luZywgVmVyc2lvbj0xLjAuMzMwMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAABVTeXN0ZW0uRHJhd2luZy5CaXRtYXABAAAABERhdGEHAgIAAAAJAwAAAA8DAAAA9gAAAAJCTfYAAAAAAAAANgAAACgAAAAIAAAACAAAAAEAGAAAAAAAAAAAAMQOAADEDgAAAAAAAAAAAAD///////////////////////////////////9ZgABZgADzPz/zPz/zPz9AgP//////////gAD/gAD/AAD/AAD/AACKyub///////+AAACAAAAAAP8AAP8AAP9AgP////////9ZgABZgABz13hz13hz13hAgP//////////gAD/gACA/wCA/wCA/wAA//////////+AAACAAAAAAP8AAP8AAP9AgP////////////////////////////////////8L"; 49 50 public MyPropertyTab() 51 { 52 } 53 54 //返回我們要顯示的屬性的信息集合 55 public override System.ComponentModel.PropertyDescriptorCollection GetProperties(object component, System.Attribute[] attributes) 56 { 57 //獲取對象所有屬性 58 PropertyDescriptorCollection props; 59 if (attributes == null) 60 props = TypeDescriptor.GetProperties(component); 61 else 62 props = TypeDescriptor.GetProperties(component, attributes); 63 64 //篩選“角度“屬性 65 int angleCount = 0; 66 for (int i = 0; i < props.Count; i++) 67 { 68 if (props[i].Category == "角度") 69 { 70 angleCount++; 71 } 72 } 73 PropertyDescriptor[] ps = new PropertyDescriptor[angleCount]; 74 int j = 0; 75 for (int i = 0; i < props.Count; i++) 76 { 77 PropertyDescriptor des = props[i]; 78 if (des.Category == "角度") 79 { 80 ps[j] = des; 81 j++; 82 } 83 } 84 85 return new PropertyDescriptorCollection(ps); 86 } 87 88 public override System.ComponentModel.PropertyDescriptorCollection GetProperties(object component) 89 { 90 return this.GetProperties(component, null); 91 } 92 93 // Tab的名字. 94 public override string TabName 95 { 96 get 97 { 98 return "AngleProperty"; 99 } 100 } 101 102 // Tab的圖標 103 public override System.Drawing.Bitmap Bitmap 104 { 105 get 106 { 107 Bitmap bmp = new Bitmap(DeserializeFromBase64Text(img)); 108 return bmp; 109 } 110 } 111 112 // 從字符串中獲取位圖 113 private Image DeserializeFromBase64Text(string text) 114 { 115 Image img = null; 116 byte[] memBytes = Convert.FromBase64String(text); 117 IFormatter formatter = new BinaryFormatter(); 118 MemoryStream stream = new MemoryStream(memBytes); 119 img = (Image)formatter.Deserialize(stream); 120 stream.Close(); 121 return img; 122 } 123 } 124 }
新建Windows工程,添加該文件,將我們的MyControl控件拖入Form中,選中控件,看我們定義的tab是否出現,再選擇Form,看是否消失。
根據本文方法,我們可以創建各種自定義屬性選項卡,顯示的內容關鍵在於你的GetProperties函數返回什么樣的值。
當然,全文純屬個人愚見,錯誤荒謬之處,還望指出,不甚感激!