閱讀本篇博客之前需要了解VS窗體設計器的工作原理,詳細可參見本系列博客(十)、(十一)、(十二)。必須需要知道的一條結論就是:處於窗體設計器(Form Designer)中的任何組件(包含控件,下同),都是實際存在的一個實例。也就是說,拖進去的button1,其實就是實例化一個Button控件。
通常編碼中,我們在使用一個類型對象時,通過以下方式:

1 Car c = new Car(); //實例化對象 2 c.Type = “標致308”; //設置屬性 3 c.Color = Color.Black; //設置屬性 4 c. InspectionInformation = new Inspection(“2013-11-12”,”張三”,”RS678T”);//設置屬性 5 c.SomethingHappened+=new SomethingHappenedEventHandler(c_SomethingHappened);//注冊事件 6 //以上為對象的初始化 以下開始使用c對象 7 //…
如上所示,我們在使用Car類時,是通過new的方式來創建一個實例,然后給它初始化一些信息,這些所有操作都是通過我們手動來編寫代碼實現的。
我們注意到,在設計UI界面的時候,窗體中的所有控件、組件都是可以通過“屬性窗體”來編輯的,也就是說,界面上這些元素的初始化不需要我們手動編寫代碼,完全可以通過點點鼠標,按按鍵盤就可以做到。我們可以總結出來設計器可以幫我們做以下工作:
- 實例化對象
沒錯,不用你手動new對象了,設計器幫你來完成。
- 編輯屬性
選定一個組件,在屬性窗體中編輯它的屬性,跟你通過編寫“實例.屬性=屬性值”是一樣的效果。
- 注冊事件
選定一個組件,在屬性窗體的事件選項卡,雙擊事件空白處,自動注冊事件。
窗體設計器不需要你手動編寫一行代碼,對象的實例化、屬性編輯、事件注冊全部搞定,也就是說,窗體設計器能夠可視化設計一些對象。至於哪些類型的對象可以通過窗體設計器來進行可視化設計,請參見本系列(十、十一、十二),我在這里直接給出結果:窗體設計器能夠可視化設計實現了IComponent接口類型的對象。也就是說,如果你定義了一個類型A,恰好它實現了IComponent接口(直接或者間接),那么你就完全可以通過窗體設計器來可視化設計A類型的對象。
由此可以看出,創建一個可以可視化設計的對象並不難,只要我們的類型實現了IComponent接口就行(官方稱這種類型為組件)。我們再來看一下,窗體設計器初始化出來的對象,跟我們自己手動編寫代碼初始化的對象有哪些相同點和不同點:
不同點:
- 前者更直觀簡單,隱藏的東西太多,后者復雜,但是清楚內部過程。
- 前者對象的初始化,在程序一啟動就開始,不能人工控制其時機,具體是在Form1的構造方法中的InitializeComponent()中進行,后者就更靈活,需要的時候編寫代碼就可以。
- 前者初始化出來的對象幾乎都跟UI界面有關(這個很容易就能想到,窗體設計器肯定設計跟窗體界面有關的東西),而后者沒有這個原則,不管是什么對象,都是可以的。
相同點:
- 都是初始化一個對象。
- 都有代碼產生,前者產生的代碼在InitializeComponent()中,后者為人工編寫。
我們應該清楚,程序最終都是要經過將源代碼編譯成可執行文件之后才能運行的,所以源代碼是一切根本,沒有源代碼,其他的都是白扯。
綜上所有之述,我們可以手動編寫代碼來初始化任何對象,我們可以通過窗體設計器來初始化實現了IComponent接口的類型對象。
好了知道怎樣才能創建一個可以可視化設計的對象之后,我們來創建一個試一下,定義一個類型MyComponent,使其繼承自Component:

1 /// <summary> 2 /// 可被 可視化設計的類,該類默認只包含屬性 3 /// </summary> 4 public partial class MyComponent : Component 5 { 6 public MyComponent() 7 { 8 InitializeComponent(); 9 } 10 11 public MyComponent(IContainer container) 12 { 13 container.Add(this); 14 InitializeComponent(); 15 } 16 /// <summary> 17 /// 字符串屬性 使用默認屬性編輯器 18 /// </summary> 19 public string StringProperty 20 { 21 set; 22 get; 23 } 24 /// <summary> 25 /// 顏色屬性 使用默認屬性編輯器 26 /// </summary> 27 public Color ColorProperty 28 { 29 set; 30 get; 31 } 32 /// <summary> 33 /// 自定義類型屬性 使用下拉列表編輯器 34 /// </summary> 35 [Editor(typeof(MyTypeEditor1),typeof(UITypeEditor)),DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] 36 public MyType MyTypeProperty1 37 { 38 set; 39 get; 40 } 41 /// <summary> 42 /// 自定義類型屬性 使用彈出對話框編輯器 43 /// </summary> 44 [Editor(typeof(MyTypeEditor2),typeof(UITypeEditor)),DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)] 45 public MyType MyTypeProperty2 46 { 47 set; 48 get; 49 } 50 /// <summary> 51 /// 控件屬性 可以在設計器中選擇已經存在的控件 52 /// </summary> 53 [Editor(typeof(ControlEditor),typeof(UITypeEditor))] 54 public Control ControlProperty 55 { 56 set; 57 get; 58 } 59 /// <summary> 60 /// ImageList類型屬性 61 /// </summary> 62 [Editor(typeof(ImageListEditor),typeof(UITypeEditor))] 63 public ImageList ImageListProperty 64 { 65 get; 66 set; 67 } 68 }
如上代碼所示,該類型只包含了幾個公共屬性,沒有其他內容。此類型對象就可以通過窗體設計器來設計了,也就是說,從工具箱中向設計器中拖放MyComponent類型之后,窗體設計器自動會實例化一個MyComponent對象,並且你可以通過屬性窗體來編輯該對象的屬性:
1)StringProperty
String類型屬性,直接可以在屬性窗體中輸入。
2)ColorProperty
Color類型屬性,屬於.NET自帶類型,所以有默認的屬性編輯器,如下圖:
圖1
3)MyTypeProperty1
自定義類型屬性,需要我們自己定義一個屬性編輯器Editor(typeof(MyTypeEditor1),typeof(UITypeEditor))。
4)MyTypeProperty2
自定義類型屬性,需要我們自己定義一個屬性編輯器Editor(typeof(MyTypeEditor2),typeof(UITypeEditor))。
5)ControlProperty
Control類型屬性,我們可以將設計器中已經存在的Control賦值給該屬性,指定了屬性編輯器Editor(typeof(ControlEditor),typeof(UITypeEditor))。
6)ImageListProperty
ImageList屬性,這個就是我們常見的一些控件(比如TabControl)含有ImageList屬性,點擊右方的小三角形,就可以列出窗體設計器中已經存在的ImageList,供你選擇。指定了屬性編輯器Editor(typeof(ImageListEditor),typeof(UITypeEditor))。
也就是說,當我們在窗體設計器中設計一個對象的時候,如果該對象包含一些特殊(非.NET默認自帶類型)類型屬性時,我們需要為該屬性提供一個“屬性編輯器”。
以下就分別為每個屬性對應的屬性編輯器了(假設諸位看官都知道了UITypeEditor的作用,不知道可以查一下):
1)MyTypeProperty1屬性

1 class MyTypeEditor1 : UITypeEditor 2 { 3 public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) 4 { 5 return UITypeEditorEditStyle.DropDown; //下拉列表 在下拉列表中輸入MyType屬性值 6 } 7 public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) 8 { 9 usrMyTypeEditor usrme = new usrMyTypeEditor(); //下拉列表 10 usrme.EditedValue = value as MyType; //初始化下拉列表框 value為舊值 11 12 IWindowsFormsEditorService ie = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; 13 if (ie != null) 14 { 15 ie.DropDownControl(usrme); //顯示下拉列表 16 return usrme.EditedValue; //返回編輯后的值 17 } 18 else 19 { 20 return value; 21 } 22 } 23 }
鼠標點擊MyTypeProperty1屬性右側的小三角形,出現一個下拉列表框。如下圖:
圖2
2)MyTypeProperty2屬性

1 class MyTypeEditor2 : UITypeEditor 2 { 3 public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) 4 { 5 return UITypeEditorEditStyle.Modal; //模式對話框 在彈出對話框中輸入MyType屬性值 6 } 7 public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) 8 { 9 using (frmMyTypeEditorForm frmmef = new frmMyTypeEditorForm()) 10 { 11 frmmef.EditedValue = value as MyType; //初始化對話框 12 IWindowsFormsEditorService ie = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; 13 if (ie != null) 14 { 15 if (ie.ShowDialog(frmmef) == DialogResult.OK) //顯示模式對話框 16 { 17 return frmmef.EditedValue; //返回編輯后的值 18 } 19 else 20 { 21 return value; //返回舊值 22 } 23 24 } 25 else 26 { 27 return value; 28 } 29 } 30 } 31 }
鼠標點擊MyTypeProperty2右側的小三角形,彈出一個對話框。
圖3
3)ControlProperty屬性

1 class ControlEditor : UITypeEditor 2 { 3 IWindowsFormsEditorService ie = null; 4 5 public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) 6 { 7 return UITypeEditorEditStyle.DropDown; //下拉列表 在下拉列表中選擇一個(設計器中已經存在)控件 8 } 9 public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) 10 { 11 ListBox li = new ListBox(); //下拉列表 12 li.Click += new EventHandler(li_Click); 13 List<Control> liCo=new List<Control>(); //下拉列表每一項對應的控件值 14 foreach(Component c in context.Container.Components) //查找窗體設計器中的每一個組件 15 { 16 if (c is Control && !(c is Form)) //若是控件 不是窗體 17 { 18 li.Items.Add((c as Control).Name); //將控件名稱寫入listbox 19 if (value as Control == c as Control) 20 { 21 li.SelectedIndex = li.Items.Count - 1; //選中原來值 22 } 23 liCo.Add(c as Control); //對應控件值寫入list 24 } 25 } 26 ie = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; 27 if (ie != null) 28 { 29 ie.DropDownControl(li); 30 if (li.SelectedIndex > -1) 31 return liCo[li.SelectedIndex]; 32 else 33 return value; 34 } 35 else 36 { 37 return value; 38 } 39 } 40 41 void li_Click(object sender, EventArgs e) 42 { 43 if (ie != null) 44 { 45 ie.CloseDropDown(); 46 } 47 } 48 }
鼠標點擊ControlProperty右側的小三角形,出現下拉列表框,列表中顯示的都是窗體設計器中已經存在的Control。
圖4
4)ImageListProperty屬性

1 class ImageListEditor : UITypeEditor 2 { 3 IWindowsFormsEditorService ie = null; 4 5 public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context) 6 { 7 return UITypeEditorEditStyle.DropDown; //下拉列表 在下拉列表中選擇一個(設計器中已經存在)ImageList組件 8 } 9 public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value) 10 { 11 ListBox li = new ListBox(); //下拉列表 12 li.Items.Add("無"); 13 li.Click += new EventHandler(li_Click); 14 List<ImageList> liCo = new List<ImageList>(); //下拉列表每一項對應的ImageList值 15 foreach (Component c in context.Container.Components) //查找窗體設計器中的每一個組件 16 { 17 if (c is ImageList) //若是ImageList 18 { 19 li.Items.Add((c as ImageList).ToString().Split(' ')[0]); //將ImageList名稱寫入listbox 20 if (value as ImageList == c as ImageList) 21 { 22 li.SelectedIndex = li.Items.Count - 1; //選中原來值 23 } 24 liCo.Add(c as ImageList); //對應ImageList寫入list 25 } 26 } 27 ie = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService; 28 if (ie != null) 29 { 30 ie.DropDownControl(li); 31 if (li.SelectedIndex == 0) //無 32 return null; 33 else if (li.SelectedIndex > 0) 34 return liCo[li.SelectedIndex-1]; 35 else 36 return value; 37 } 38 else 39 { 40 return value; 41 } 42 } 43 44 void li_Click(object sender, EventArgs e) 45 { 46 if (ie != null) 47 { 48 ie.CloseDropDown(); 49 } 50 } 51 }
鼠標點擊ImageListProperty右側的小三角形,出現下拉列表,列表中顯示窗體設計器中已經存在的ImageList。
圖5
本篇博客介紹了怎么創建一個可以可視化設計的對象(准確來講,應該是怎樣創建一個可以可視化設計其對象的類型),首先讓類型(間接或者直接)實現IComponent接口(不要問我為什么,請參見前面的博客),然后為類型的某些特殊屬性創建對應的屬性編輯器,就這么簡單。類型創建者工作量大一點,但是類型使用者更方便一些,Demo中MyComponent類型就是最后的核心成果,供使用者使用,使用者不需要知道其他的類似屬性編輯器之類的東西,在他們看來,這些相當於沒有。
注意使用范圍,並不是任何時候都可以使用,最好參見前面提到的“相同點”和“不同點”那里。另外,本篇博客中設計到的知識點很多,像provider.GetService(typeof(IWindowsFormsEditorService))、foreach(Component c in context.Container.Components)這些需要和設計器打交道的地方我幾乎沒有詳細說到過,原因是這些太復雜了,要是細說的話,得說一大簍子,而且不是本篇文章的重點。
源碼下載地址:http://files.cnblogs.com/xiaozhi_5638/PropertyEditorInDesigner.rar
注意不要試圖運行源代碼,沒有任何效果,只能在設計器中看到效果。希望有幫助。
Update (2013-12-09)
上面結束提到了“支持可視化設計對象”的使用場合,建議如果該類型跟UI界面有關聯(需要與界面其他元素交互),並且對象的實例化不需要人工控制其時機,那么可以讓該類型支持可視化設計(也就是實現IComponent接口)。注意這里的建議,也就是說不是強制性的,你完全可以定義一個People類,讓其實現IComponent接口,這合法!結果就是,從工具欄中拖放一個People到窗體設計器中,它會自動幫你實例化了一個People對象實例,並且你也能通過屬性窗體編輯它的屬性值(這些后台都能自動生成對應代碼)。另外,經發現,具備某一些功能的類型,雖然跟界面無交互,但是它還是支持可視化設計,比如BackgroundWorker組件,拖一個BackgroundWorker到窗體設計器中去,設計器會自動幫你實例化一個Backgroundworker對象(生成對應代碼),然后你可以通過屬性窗體編輯它的屬性值(生成對應代碼),這跟你自己手動new BackgroundWorker沒有區別,你還是可以在其他地方一樣使用該backgroundworke對象,至少你目前看起來沒區別。
為了更好的說明通過窗體設計器設計出來的對象,跟我們手動編寫代碼搞出來的對象有什么相同和不同點,我們先來分析一下Form1.Designer.cs中的代碼:
我們注意到,我們在設計器中的每一步操作,對應生成的代碼都在InitializeComponent()方法中,它像是word錄制宏的功能,你在word中的每一個操作,都可以生成對應的VBA代碼,也就是說,窗體設計器從實例化對象,到編輯屬性,再到注冊事件等等等,都有代碼幫我們記錄下這些操作。我們再看一下實例化對象的代碼:

1 this.myComponent1 = new PropertyEditorInDesigner.MyComponent(this.components);
沒錯,任何一個實例化出來的對象,都給它傳遞了this.components容器,我們再看MyComponent的構造方法是這樣的:

1 public MyComponent(IContainer container) 2 { 3 container.Add(this); 4 InitializeComponent(); 5 }
也就是說,對象實例化的時候,都將該對象放進了一個components的容器,這個容器專門用來存放由窗體設計器實例化出來的對象(控件除外)。這就像一個大的容器,專門來存放這些小個體。接下來我們再來看一下Form1.Designer.cs中的dispose方法:

1 protected override void Dispose(bool disposing) 2 { 3 if (disposing && (components != null)) 4 { 5 components.Dispose(); 6 } 7 base.Dispose(disposing); 8 }
我們可以發現,在Form1對象Dispose的時候,它將components中的所有個體都Dispose掉了。到此,我們可以總結出來一條:由窗體設計器設計出來的對象由父窗體(父控件)的InitializeComponents方法統一初始化,由父窗體(父控件)的Dispose()方法統一釋放資源。我們來看一張Form1運行結構圖:
圖(更新)1
由此可以看出,由窗體設計器設計出來的對象,它們的生命周期以及存放結構都比較有規律,這個就是窗體設計器設計出來對象的好處。
我們在往窗體設計器中拖放組件時,就是往Form1類型中添加新的成員對象,跟我們定義一個類型,向里面添加成員變量一個意思,只是前者更直觀,添加的每一個成員對象,在UI設計器中都能看見與它對應的一個對象實例,只要我們改變了這個對象實例的屬性,就能馬上看見設計器中的對象實例效果,緊接着后台生成代碼;而后者就沒有這么直觀了,只能手寫代碼,而且還看不見效果。
圖(更新)2
如上圖,窗體設計器中某一個對象實例屬性更新之后,馬上就能在設計器中看見效果(因為它是實實在在存在於堆中的對象),接着InitializeComponents中的代碼就會更新,注意,窗體設計器中的myComponent1對象實例跟.cs代碼中的myComponent1變量不是一個東西,他們只有一種映射關系。我們最終要的是.cs中的代碼文件,而不是我們看見的窗體設計器中的圖像,后者只是起到一個可視化的效果,最終一文不值。這個就像photoshop作圖一樣,最終保存到硬盤的圖片文件才是最重要的,作圖過程中作圖區域顯示的東西沒有價值。窗體設計器隱藏得越多,我們知道得越少。